From: chaoskagami Date: Tue, 19 Aug 2014 12:01:19 +0000 (-0400) Subject: Initial commit X-Git-Tag: 1.2.5-linux-uo X-Git-Url: https://chaos.moe/g/?a=commitdiff_plain;h=91d1d658b9eadab219fed479282cdb5c077fa379;p=vn%2FFSNConverter-Linux.git Initial commit --- 91d1d658b9eadab219fed479282cdb5c077fa379 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..1dce76e --- /dev/null +++ b/COPYING @@ -0,0 +1,343 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +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 give any other recipients of the Program a copy of this License +along with the Program. + +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 Program or any portion +of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +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 Program, 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 Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) 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; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, 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 executable. However, as a +special exception, the source code 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. + +If distribution of executable or 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 counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program 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. + + 5. 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 Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program 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 to +this License. + + 7. 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 Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program 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 Program. + +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. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program 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. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 Program +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 Program does not specify a +version number of this License, you may choose any version ever +published by the Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, 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 + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "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 +PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. 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 PROGRAM 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 +PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/Distrib/FSNConverter.sh b/Distrib/FSNConverter.sh new file mode 100755 index 0000000..1106aeb --- /dev/null +++ b/Distrib/FSNConverter.sh @@ -0,0 +1,3 @@ +#!/bin/bash +export PATH=$PATH:`pwd`/bin +java -jar FSNConverter.jar diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..c9622a5 --- /dev/null +++ b/README.txt @@ -0,0 +1,49 @@ +What is this? + +- Yet another one of my random portish things. The Fate/Stay night VNDS +converter at first glance looked very unixy-okay, so I figured I might +be able to just run it on linux. +- Not exactly. + +Here was my list of concerns: + - Bundled tools. + - Java code putting C: and Z: in automatically. + - No regard for system tools. + - Source code is windows specific. + - Launch4j needs butchering. + +Basically, I've fixed all of these imperfectly. Enough to use it to do +its job. As proof, the fate version I'm playing right now was a byproduct of +this. + + - Misc tools converted. Lotta windows calls ugh + - Java code altered a bit. Some things needed to go. + - tools has been renamed to bin, and we append it to the path instead. + - System tools preferred, and in most cases, required. + +Install these deps or you won't be getting anywhere: + - pngcrush + - ffmpeg/libav + - liboggz + - ant + - java1.6 +These are optional. + - pngquant + - pngnq + +FAQ: + Q: So let me get this straight. Fate is a windows game, and you're + running a tool on linux to convert for android. WTF is wrong with + you. + A: There's this thing called a 'canadian-cross' compiler. This is like + that. So no, not crazy. + + Q: Does this work? + A: For the most part. During conversion I DID get a few warnings spat + out about some tlg files. + + Q: I never managed to get fate to run on wine. HELP + A: Install Dx9. Also, you probably won't get the videos working so rename + video.xp3 to video.xp3.disabled or something. + +For license read COPYING. tl;dr same as weaboo.nl, since I can't change the license ( GPLv2 ) diff --git a/Tools/ahx2wav/COPYING b/Tools/ahx2wav/COPYING new file mode 100644 index 0000000..b6f92f3 --- /dev/null +++ b/Tools/ahx2wav/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +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 give any other recipients of the Program a copy of this License +along with the Program. + +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 Program or any portion +of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +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 Program, 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 Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) 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; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, 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 executable. However, as a +special exception, the source code 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. + +If distribution of executable or 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 counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program 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. + + 5. 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 Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program 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 to +this License. + + 7. 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 Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program 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 Program. + +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. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program 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. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 Program +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 Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, 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 + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/Tools/ahx2wav/ahx2wav.c b/Tools/ahx2wav/ahx2wav.c new file mode 100644 index 0000000..b5229b8 --- /dev/null +++ b/Tools/ahx2wav/ahx2wav.c @@ -0,0 +1,609 @@ +/* + Copyright (C) 2006 + http://pie.bbspink.com/test/read.cgi/leaf/1141063964/191 + http://pie.bbspink.com/test/read.cgi/leaf/1141063964/258 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +*/ + +// mpeg2 audio layer-2‚̃fƒR[ƒh•û–@‚Ímpg123‚ðŽQl‚É‚µ‚Ü‚µ‚½ +// http://www.mpg123.de/ +#include +#include +#include +#include +#include +#include +#include + +#define AHXHED "\x80\x00\x00\x20\x11\x00\x00" //AHXƒwƒbƒ_‚Ì’è‹` +#define AHXCHK "(c)CRI" //Šm”F—pAHXƒwƒbƒ_‚Ì’è‹` +#define AHXFOT "AHXE(c)CRI" //AHXƒtƒbƒ^‚Ì’è‹` +#define OPT_d 0x01 //ƒfƒBƒŒƒNƒgƒŠì¬ƒIƒvƒVƒ‡ƒ“ + +int getopt(int ac, char **av, char *opts); +extern int opterr, optind, optopt; +extern char *optarg; + +void *memmem (const void *base,int count,const void *pattern,int length) +{ + const char *start; + const char *p; + if ( count <= 0 ) + return NULL; + start = (const char *)base; + for( p = start; p + length <= start + count; ++p ) { + if ( 0 == memcmp( p, pattern, length ) ) + return (char *)p; + } + return NULL; +} //memmem + + +unsigned long getword(unsigned char *p) +{ + return p[0]+p[1]*256; +} //modify for bcc32 + +unsigned long getlong(unsigned char *p) +{ + return p[0]+p[1]*256+(p[2]+p[3]*256)*65536; +} //modify for bcc32 + +unsigned long getlongb(unsigned char *p) +{ + return p[3]+p[2]*256+(p[1]+p[0]*256)*65536; +} //modify for bcc32 + +void putword(unsigned char *p,int a) +{ + p[0] = a; + p[1] = a>>8; +} //modify for bcc32 + +void putlong(unsigned char *p,int a) +{ + p[0] = a; + p[1] = a>>8; + p[2] = a>>16; + p[3] = a>>24; +} //modify for bcc32 + +int bit_alloc_table[32] = { 4,4,4,4,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 }; +int offset_table[5][16] = { + { 0}, + { 0}, + { 0, 1, 3, 4, }, + { 0, 1, 3, 4, 5, 6, 7, 8, }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 } +}; + +struct quantclass { + int nlevels; + int bits; +} const qc_table[17] = { + { 3, -5}, + { 5, -7}, + { 7, 3}, + { 9, -10}, + { 15, 4}, + { 31, 5}, + { 63, 6}, + { 127, 7}, + { 255, 8}, + { 511, 9}, + { 1023, 10}, + { 2047, 11}, + { 4095, 12}, + { 8191, 13}, + { 16383, 14}, + { 32767, 15}, + { 65535, 16} +}; +int intwinbase[257] = { + 0, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -3, -3, -4, -4, -5, + -5, -6, -7, -7, -8, -9, -10, -11, -13, -14, -16, -17, -19, -21, -24, -26, + -29, -31, -35, -38, -41, -45, -49, -53, -58, -63, -68, -73, -79, -85, -91, -97, + -104, -111, -117, -125, -132, -139, -147, -154, -161, -169, -176, -183, -190, -196, -202, -208, + -213, -218, -222, -225, -227, -228, -228, -227, -224, -221, -215, -208, -200, -189, -177, -163, + -146, -127, -106, -83, -57, -29, 2, 36, 72, 111, 153, 197, 244, 294, 347, 401, + 459, 519, 581, 645, 711, 779, 848, 919, 991, 1064, 1137, 1210, 1283, 1356, 1428, 1498, + 1567, 1634, 1698, 1759, 1817, 1870, 1919, 1962, 2001, 2032, 2057, 2075, 2085, 2087, 2080, 2063, + 2037, 2000, 1952, 1893, 1822, 1739, 1644, 1535, 1414, 1280, 1131, 970, 794, 605, 402, 185, + -45, -288, -545, -814, -1095, -1388, -1692, -2006, -2330, -2663, -3004, -3351, -3705, -4063, -4425, -4788, + -5153, -5517, -5879, -6237, -6589, -6935, -7271, -7597, -7910, -8209, -8491, -8755, -8998, -9219, -9416, -9585, + -9727, -9838, -9916, -9959, -9966, -9935, -9863, -9750, -9592, -9389, -9139, -8840, -8492, -8092, -7640, -7134, + -6574, -5959, -5288, -4561, -3776, -2935, -2037, -1082, -70, 998, 2122, 3300, 4533, 5818, 7154, 8540, + 9975, 11455, 12980, 14548, 16155, 17799, 19478, 21189, 22929, 24694, 26482, 28289, 30112, 31947, 33791, 35640, + 37489, 39336, 41176, 43006, 44821, 46617, 48390, 50137, 51853, 53534, 55178, 56778, 58333, 59838, 61289, 62684, + 64019, 65290, 66494, 67629, 68692, 69679, 70590, 71420, 72169, 72835, 73415, 73908, 74313, 74630, 74856, 74992, + 75038 }; +double costable[5][16],decwin[512+32]; + +int getbits(unsigned char **src, int *bit_data, int *bit_rest, int bits) +{ + int __ret; + + while(*bit_rest<24){ + *bit_data<<=8; + *bit_data|=**src; + *src=*src+1; + *bit_rest+=8; + } + + __ret = (*bit_data>>(*bit_rest-bits)) & ((1<> 2]]; //modify : from ExtractData + sbsamples[gr * 3 + 1][sb] *= powtable[scalefactor[sb][gr >> 2]]; //modify : from ExtractData + sbsamples[gr * 3 + 2][sb] *= powtable[scalefactor[sb][gr >> 2]]; //modify : from ExtractData + } + } + // synth + for(gr=0;gr<36;gr++){ + double sum=0; + if(phase&1) + dct(sbsamples[gr],dctbuf[0][phase+1&15],dctbuf[1][phase]); + else + dct(sbsamples[gr],dctbuf[1][phase],dctbuf[0][phase+1]); + win = decwin + 16-(phase|1); + for(i=0;i<16;i++,win+=16){ + sum = *win++ * dctbuf[phase&1][0][i]; + sum -= *win++ * dctbuf[phase&1][1][i]; + sum += *win++ * dctbuf[phase&1][2][i]; + sum -= *win++ * dctbuf[phase&1][3][i]; + sum += *win++ * dctbuf[phase&1][4][i]; + sum -= *win++ * dctbuf[phase&1][5][i]; + sum += *win++ * dctbuf[phase&1][6][i]; + sum -= *win++ * dctbuf[phase&1][7][i]; + sum += *win++ * dctbuf[phase&1][8][i]; + sum -= *win++ * dctbuf[phase&1][9][i]; + sum += *win++ * dctbuf[phase&1][10][i]; + sum -= *win++ * dctbuf[phase&1][11][i]; + sum += *win++ * dctbuf[phase&1][12][i]; + sum -= *win++ * dctbuf[phase&1][13][i]; + sum += *win++ * dctbuf[phase&1][14][i]; + sum -= *win++ * dctbuf[phase&1][15][i]; + if(sum>=32767) *dst_p ++ = 32767; + else if(sum<=-32767) *dst_p ++ = -32767; + else *dst_p ++ = sum; + } + sum = win[0] * dctbuf[phase&1][0][16]; + sum += win[2] * dctbuf[phase&1][2][16]; + sum += win[4] * dctbuf[phase&1][4][16]; + sum += win[6] * dctbuf[phase&1][6][16]; + sum += win[8] * dctbuf[phase&1][8][16]; + sum += win[10] * dctbuf[phase&1][10][16]; + sum += win[12] * dctbuf[phase&1][12][16]; + sum += win[14] * dctbuf[phase&1][14][16]; + if(sum>=32767) *dst_p ++ = 32767; + else if(sum<=-32767) *dst_p ++ = -32767; + else *dst_p ++ = sum; + win += -16 + (phase|1)*2; + for(i=15;i>=1;i--,win-=16){ + sum = -*--win * dctbuf[phase&1][0][i]; + sum -= *--win * dctbuf[phase&1][1][i]; + sum -= *--win * dctbuf[phase&1][2][i]; + sum -= *--win * dctbuf[phase&1][3][i]; + sum -= *--win * dctbuf[phase&1][4][i]; + sum -= *--win * dctbuf[phase&1][5][i]; + sum -= *--win * dctbuf[phase&1][6][i]; + sum -= *--win * dctbuf[phase&1][7][i]; + sum -= *--win * dctbuf[phase&1][8][i]; + sum -= *--win * dctbuf[phase&1][9][i]; + sum -= *--win * dctbuf[phase&1][10][i]; + sum -= *--win * dctbuf[phase&1][11][i]; + sum -= *--win * dctbuf[phase&1][12][i]; + sum -= *--win * dctbuf[phase&1][13][i]; + sum -= *--win * dctbuf[phase&1][14][i]; + sum -= *--win * dctbuf[phase&1][15][i]; + if(sum>=32767) *dst_p ++ = 32767; + else if(sum<=-32767) *dst_p ++ = -32767; + else *dst_p ++ = sum; + } + phase = phase-1 & 15; + } + // skip padding bits + if(bit_rest&7) getbits(&src, &bit_data, &bit_rest, bit_rest&7); + } + //printf("%d frames\n",frame); + //printf("after : dst_p=%d, *dst_p=%d\n", dst_p, *dst_p); + //printf("after : dst=%d, *dst=%d\n", dst, *dst); + return ((char*)dst_p - (char*)dst); //modify : from ExtractData +} + +void fwrite_wav_hed(int len,int freq,FILE *fp) +{ + static unsigned char hed[0x2c] = { + 'R','I','F','F',0,0,0,0, + 'W','A','V','E','f','m','t',' ',0x10,0,0,0, + 0x01,0x00, // Linier PCM + 0x02,0x00, // ch + 0x44,0xac,0x00,0x00, // 44.1kHz + 0x10,0xb1,0x02,0x00, // 44.1*4B + 0x04,0x00, // block size + 0x10,0x00, // bit/sample + 'd','a','t','a',0,0,0,0 + }; + putword(hed+0x16,1); // ch + putlong(hed+0x18,freq); // freq + putlong(hed+0x1c,freq*2); // byte/s + putword(hed+0x20,0x02); // block size + putword(hed+0x22,0x10); // bit/sample + + putlong(hed+4,len+0x2c-4); + putlong(hed+0x28,len); + + fwrite(hed,1,0x2c,fp); +} + +#define MAX_PATH 4096 + +int main(int argc,char **argv) +{ + FILE *fp; + DIR *fd; + unsigned char *src_buf, *ahx_buf, *wav_buf; + unsigned char *src_buf_set, *ahx_hed, *ahx_fot; + char wav_fn[MAX_PATH], wav_dir[MAX_PATH], src_dir[MAX_PATH]; //modify for ahx2wav + int src_buf_len, src_buf_rem, ahx_buf_len, wav_buf_len; //modify for ahx2wav + int ahx_count=0, opt=0; + char teststr[6]; + + if(argc<2) { + printf("usage : ahx2wav [-d] filename\nexample : ahx2wav hoge.ahx\n"); + exit(1); + } //modify for ahx2wav + + opterr = 0; //getopt ƒGƒ‰[”ñ•\ަ + while( getopt(argc, argv, "dl:") != EOF ) { + switch(optopt) { + case 'd': + opt = opt | OPT_d; + break; + default: + printf("unknown option -%c\n", optopt); + exit(2); + } + } + + argv = argv + optind; //ƒtƒ@ƒCƒ‹–¼ŠJŽnˆÊ’u‚ɃZƒbƒg + + if (opt & OPT_d) { + // Enumerate over files + fd = opendir(*argv); + + if ( !fd ) { + printf("Directory handle invalid."); + } + else { + struct dirent *ep; + + do { + ep = readdir(fd); + + struct stat st; + + lstat(ep->d_name, &st); + + if(! S_ISDIR(st.st_mode)) { + fp = fopen(ep->d_name, "rb"); + if( fp == NULL) { + printf("Can't open %s.\n", ep->d_name); + } + else if ( !(strcmp(ep->d_name, "ahx2wav.exe")) ) + fclose(fp); + else { + getcwd(src_dir, MAX_PATH); + + fseek(fp,0,SEEK_END); + src_buf_len = src_buf_rem = ftell(fp); + src_buf_set = malloc(src_buf_len); + fseek(fp,0,SEEK_SET); + fread(src_buf_set,1,src_buf_len,fp); + src_buf = src_buf_set; + fclose(fp); + + while(src_buf_rem>0) { + ahx_hed=memmem(src_buf,src_buf_rem,AHXHED,sizeof(AHXHED)-1); + ahx_fot=memmem(src_buf,src_buf_rem,AHXFOT,sizeof(AHXFOT)-1); + if(ahx_hed==NULL || ahx_fot==NULL) + src_buf_rem=0; + else if (strncmp(ahx_hed+30, AHXCHK, sizeof(AHXCHK)-1)!=0) { + src_buf++; + src_buf_rem--; + } + else { + ahx_count++; + + ahx_buf_len=ahx_fot+12-ahx_hed; + ahx_buf=malloc(ahx_buf_len); + memcpy(ahx_buf,ahx_hed,ahx_buf_len); + wav_buf_len=getlongb(ahx_buf+12)*2; + wav_buf=malloc(wav_buf_len+1152*16); //modify : from ExtractData; //+1152*2); margen = layer-2 frame size + wav_buf_len=decode_ahx(ahx_buf,wav_buf,ahx_buf_len); //modify : from ExtractData + + if( opt & OPT_d ) { //ƒtƒHƒ‹ƒ_•ʏo—Í + sprintf(wav_dir, "_%s", ep->d_name); + mkdir(wav_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + chdir(wav_dir); + } + else + strcpy(wav_dir, src_dir); + sprintf(wav_fn, "%s_%03d.wav", ep->d_name,ahx_count); + + fp=fopen(wav_fn,"wb"); + fwrite_wav_hed(wav_buf_len,getlongb(ahx_buf+8),fp); + fwrite(wav_buf,1,wav_buf_len,fp); + //printf("%s > %s\\%s\n", fd->cFileName, wav_dir, wav_fn); + fclose(fp); + free(ahx_buf); + free(wav_buf); + + src_buf=ahx_fot+12; + src_buf_rem=src_buf_len-(src_buf-src_buf_set); + chdir(src_dir); + } + } + if ( ahx_count>0 ) + printf("%s is done.\n", ep->d_name); + ahx_count=0; + free(src_buf_set); + } + } + } while( *++argv != NULL ); + } + } + else { + + } + return 0; +} diff --git a/Tools/ahx2wav/getopt.c b/Tools/ahx2wav/getopt.c new file mode 100644 index 0000000..82e991d --- /dev/null +++ b/Tools/ahx2wav/getopt.c @@ -0,0 +1,65 @@ +/*LINTLIBRARY*/ +/*********************************************************************** + getopt.c ‚±‚Ìgetopt.c‚ÍPublic Domain‚Å‚· + 1996.4 Hiroshi Masuda •ύX +***********************************************************************/ +#include +#include + +int opterr = 1; /* getopt()ƒGƒ‰[ƒƒbƒZ[ƒW 1:•\ަ 0:”ñ•\ަ */ +int optind = 1; /* getopt()ƒCƒ“ƒfƒbƒNƒX */ +int optopt; /* Žæ“¾ƒIƒvƒVƒ‡ƒ“•¶Žš */ +char *optarg; /* Žæ“¾ƒIƒvƒVƒ‡ƒ“ƒpƒ‰ƒ[ƒ^•¶Žš—ñ */ + +static void errdisp(char *cmd, char *as) +{ + static char crtail[ ] = {0, '\n', 0}; + + if(opterr){ + fputs(cmd, stderr); fputs(as, stderr); + *crtail = optopt; fputs(crtail, stderr); + } +} + +int getopt(int ac, char **av, char *opts) +{ + static char *curopt = NULL; + register char *cp; + + if(curopt == NULL || !*curopt){ + curopt = av[optind]; + if(optind >= ac || *curopt != '-' || !curopt[1]) + return(EOF); /* ƒIƒvƒVƒ‡ƒ“Žw’è‚È‚µ */ + if(!strcmp("-", ++curopt)){ + ++optind; + return(EOF); /* -- Žw’è */ + } + } + optopt = *curopt++; /* option•¶ŽšŠi”[ */ + cp = strchr(opts, optopt); /* option•¶ŽšŒŸõ */ + if(optopt == ':' || cp == NULL){ /* option•¶Žš•s³ or ŒŸõޏ”s */ + errdisp(*av, ": unknown option, -"); + if(!*curopt) + ++optind; + return('?'); + } + if(*++cp == ':'){ /* optionƒpƒ‰ƒ[ƒ^‚ ‚è */ + ++optind; + if(*curopt){ + optarg = curopt; + curopt = NULL; + }else{ + if(optind >= ac){ + errdisp(*av, ": argument missing for -"); + return('?'); + }else{ + optarg = av[optind++]; + } + /* now *curopt == '\0' */ + } + }else{ + optarg = NULL; + if(!*curopt) ++optind; + } + return(optopt); +} diff --git a/Tools/ahx2wav/make.sh b/Tools/ahx2wav/make.sh new file mode 100755 index 0000000..fc19318 --- /dev/null +++ b/Tools/ahx2wav/make.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +F=$1 + +if [ "$F" == "ahx2wav" ]; then + gcc -c ahx2wav.c + gcc -c getopt.c + gcc -lm getopt.o ahx2wav.o -o ahx2wav +elif [ "$F" == "clean" ]; then + rm getopt.o ahx2wav.o ahx2wav +fi diff --git a/Tools/ima2raw/ima2raw.cpp b/Tools/ima2raw/ima2raw.cpp new file mode 100644 index 0000000..515685b --- /dev/null +++ b/Tools/ima2raw/ima2raw.cpp @@ -0,0 +1,165 @@ +#include +#include +#include + +#define WAV_FORMAT_IMA_ADPCM 0x14 +#define CD_BUFFER_SIZE 8192 +#define CD_BUFFER_CHUNK_SIZE (CD_BUFFER_SIZE >> 2) + +#define u32 unsigned int +#define u16 unsigned short + +#define s32 int +#define s16 short + +#define MAX_PATH 260 + +struct CD_WaveHeader +{ + char riff[4]; // 'RIFF' + u32 size; // Size of the file + char wave[4]; // 'WAVE' + + // fmt chunk + char fmt[4]; // 'fmt ' + u32 fmtSize; // Chunk size + u16 fmtFormatTag; // Format of this file + u16 fmtChannels; // Num channels + u32 fmtSamPerSec; // Samples per second + u32 fmtBytesPerSec; // Bytes per second + u16 fmtBlockAlign; // Block alignment + u16 fmtBitsPerSam; // Bits per sample + + u16 fmtExtraData; // Number of extra fmt bytes + u16 fmtExtra; // Samples per block (only for IMA-ADPCM files) +}; // __attribute__ ((packed)); + +struct CD_chunkHeader +{ + char name[4]; + u32 size; +}; // __attribute__ ((packed)); + +struct CD_Header +{ + s16 firstSample; + char stepTableIndex; + char reserved; +}; // __attribute__ ((packed)); + +struct CD_decoderFormat +{ + s16 initial; + unsigned char tableIndex; + unsigned char test; + unsigned char sample[1024]; +}; // __attribute__ ((packed)); + +bool CD_active = false; +CD_WaveHeader CD_waveHeader; +CD_Header CD_blockHeader; +FILE* CD_file; +int CD_fillPos; +bool CD_isPlayingFlag = false; + +s16* CD_audioBuffer; +u32 CD_sampleNum; +s16* CD_decompressionBuffer; +int CD_numLoops; +int CD_blockCount; +int CD_dataChunkStart; + +// These are from Microsoft's document on DVI ADPCM +const int CD_stepTab[ 89 ] = +{ + 7, 8, 9, 10, 11, 12, 13, 14, + 16, 17, 19, 21, 23, 25, 28, 31, + 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, + 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, + 724, 796, 876, 963, 1060, 1166, 1282, 1411, + 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, + 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, + 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 +}; + +const int CD_indexTab[ 16 ] = { -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 }; + +int main(int argc, char *argv[]) +{ + if(argc < 2) + { + printf("too few parameters - need a filename\n"); + return 0; + } + + char filename[MAX_PATH]; + strcpy(filename, argv[1]); + FILE* inFile = fopen(filename, "rb"); + + if(inFile == NULL) + { + printf("file not found - %s\n", filename); + return 0; + } + + char outfilename[MAX_PATH]; + sprintf(outfilename, "%s.raw", filename); + FILE* outFile = fopen(outfilename, "wb"); + + if(outFile == NULL) + { + printf("%s could not be created\n", outfilename); + fclose(inFile); + return 0; + } + + fread((void*)&CD_waveHeader, sizeof(CD_waveHeader), 1, inFile); + + printf("In file:\n"); + printf("Format: %d\n", CD_waveHeader.fmtFormatTag); + printf("Rate : %d\n", CD_waveHeader.fmtSamPerSec); + printf("Bits : %d\n", CD_waveHeader.fmtBitsPerSam); + printf("BlkSz : %d\n", CD_waveHeader.fmtExtra); + + if((CD_waveHeader.fmtFormatTag != 17) && (CD_waveHeader.fmtFormatTag != 20)) + { + printf("Wave file is in the wrong format! You must use IMA-ADPCM 4-bit mono.\n"); + return 0; + } + + // Skip chunks until we reach the data chunk + CD_chunkHeader chunk; + fread((void*)&chunk, sizeof(CD_chunkHeader), 1, inFile); + + while(!((chunk.name[0] == 'd') && (chunk.name[1] == 'a') && (chunk.name[2] == 't') && (chunk.name[3] == 'a'))) + { + fseek(inFile, chunk.size, SEEK_CUR); + fread((void*)&chunk, sizeof(CD_chunkHeader), 1, inFile); + } + + int blocksize = CD_waveHeader.fmtBlockAlign - sizeof(CD_blockHeader); + char* block = (char*)malloc(blocksize); + + int firstblock = 1; + + while( !feof(inFile) ) + { + fread((void*)&CD_blockHeader, sizeof(CD_blockHeader), 1, inFile); + if(firstblock) + { + fwrite((const void*)&CD_blockHeader, sizeof(CD_blockHeader), 1, outFile); + firstblock = 0; + } + + size_t bytesread = fread(block, 1, blocksize, inFile); + fwrite(block, 1, bytesread, outFile); + } + + free(block); + fclose(inFile); + fclose(outFile); +} diff --git a/Tools/tlg2bmp/tlg2bmp.cpp b/Tools/tlg2bmp/tlg2bmp.cpp new file mode 100644 index 0000000..2a0ffc1 --- /dev/null +++ b/Tools/tlg2bmp/tlg2bmp.cpp @@ -0,0 +1,247 @@ +#include +#include +#include +#include + +typedef int32_t LONG; +typedef uint32_t DWORD; +typedef uint8_t BYTE; +typedef uint16_t WORD; + +typedef struct tagBITMAPINFOHEADER { + DWORD biSize; //4 + LONG biWidth; //8 + LONG biHeight; //12 + WORD biPlanes; //14 + WORD biBitCount; //16 + DWORD biCompression; //20 + DWORD biSizeImage; //24 + LONG biXPelsPerMeter; //28 + LONG biYPelsPerMeter; //32 + DWORD biClrUsed; //36 + DWORD biClrImportant; //40 +} __attribute__((packed)) BITMAPINFOHEADER, *PBITMAPINFOHEADER; + +typedef struct tagBITMAPFILEHEADER { + WORD bfType; + DWORD bfSize; + WORD bfReserved1; + WORD bfReserved2; + DWORD bfOffBits; +} __attribute__((packed)) BITMAPFILEHEADER, *PBITMAPFILEHEADER; + +typedef unsigned char uchar; + +typedef struct { + char signature[11]; + uchar colors; + LONG width; + LONG height; + LONG blockheight; +} __attribute__((packed)) TLGHeader; + +typedef struct { + uchar flag; + LONG size; +} __attribute__((packed)) BlockHeader; + +void Convert(FILE* input, FILE* output); + +int main(int argc, char** argv) +{ + if (argc < 2){ + printf("%s inputfile outputfile\n", argv[0]); + return 0; + } + char* inputfile = argv[1]; + char* outputfile = argv[2]; + + FILE* f = fopen(inputfile, "rb"); + FILE* o = fopen(outputfile, "wb"); + if (f == NULL || o == NULL){ + fprintf(stderr, "fopen error\n"); + return 1; + } + + Convert(f, o); + fclose(f); + fclose(o); + return 0; +} + + +int DecodeLZSS(uchar* out, uchar* in, int insize, uchar* text, int initialr) +{ + int r = initialr; + unsigned int flags = 0; + const uchar* inlim = in + insize; + while(in < inlim) + { + if(((flags >>= 1) & 256) == 0) + { + flags = *in++ | 0xff00; + } + if(flags & 1) + { + int mpos = in[0] | ((in[1] & 0xf) << 8); + int mlen = (in[1] & 0xf0) >> 4; + in += 2; + mlen += 3; + if(mlen == 18) mlen += 0[in++]; + + while(mlen--) + { + *out++ = text[r++] = text[mpos++]; + mpos &= (4096 - 1); + r &= (4096 - 1); + } + } + else + { + unsigned char c = 0[in++]; + *out++ = c; + text[r++] = c; +/* 0[out++] = text[r++] = 0[in++];*/ + r &= (4096 - 1); + } + } + return r; +} + + + +void Convert(FILE* input, FILE* output) +{ + TLGHeader header; + fread(&header, 1, sizeof(header), input); + + if(0 != memcmp("TLG5.0\x00raw\x1a", header.signature, sizeof(header.signature))){ + fprintf(stderr, "signature error"); + exit(1); + } + if (header.colors != 3 && header.colors != 4){ + fprintf(stderr, "unsupported colors"); + exit(1); + } + + // write bitmap header + BITMAPFILEHEADER bf; + BITMAPINFOHEADER bi; + memset(&bf, 0, sizeof(bf)); + memset(&bi, 0, sizeof(bi)); + + bf.bfType = 'B' | ('M' << 8); + bf.bfOffBits = sizeof(bf) + sizeof(bi); + + bi.biSize = sizeof(bi); + bi.biWidth = header.width; + bi.biHeight = header.height; + bi.biPlanes = 1; + bi.biBitCount = header.colors * 8; + + fwrite(&bf, 1, sizeof(bf), output); + fwrite(&bi, 1, sizeof(bi), output); + int imagestart = ftell(output); + + // skip tlg block size sectionblock + long blockcount = ((header.height - 1) / header.blockheight) + 1; + fseek(input, 4*blockcount, SEEK_CUR); + + uchar* outbuf[4]; + uchar* text = (uchar*)malloc(sizeof(uchar) * 4096); + memset(text, 0, 4096); + uchar* inbuf = (uchar*)malloc(sizeof(uchar) * header.blockheight * header.width + 10); + for (int i=0; i < header.colors; i++){ + outbuf[i] = (uchar*)malloc(sizeof(uchar) * header.blockheight * header.width + 10); + } + + int r = 0; + + uchar *prevline = (uchar*)malloc(sizeof(uchar) * header.width * header.colors); + memset(prevline, 0, header.width * header.colors); + for(int y_blk = 0; y_blk < header.height; y_blk += header.blockheight) + { + // read file and decompress + int i; + for(i = 0; i < header.colors; i++) + { + BlockHeader h; + + fread(&h, 1, sizeof(h), input); + if(h.flag == 0) + { + // modified LZSS compressed data + fread(inbuf, 1, h.size, input); + r = DecodeLZSS(outbuf[i], inbuf, h.size, text, r); + } + else + { + // raw data + fread(outbuf[i], 1, h.size, input); + } + } + + // compose colors and store + int y_lim = y_blk + header.blockheight; + if(y_lim > header.height) + y_lim = header.height; + + uchar* outbufp[4]; + for(i = 0; i < header.colors; i++) + outbufp[i] = outbuf[i]; + for(int y = y_blk; y < y_lim; y++) + { + uchar* prevp = prevline; + uchar pr = 0, pg = 0, pb = 0, pa = 0; + int x = 0; + fseek(output, imagestart + header.colors*header.width*(header.height - (y+1)), SEEK_SET); + switch(header.colors) + { + case 3: + for(pr = 0, pg = 0, pb = 0, x = 0; + x < header.width; x++) + { + int b = outbufp[0][x]; + int g = outbufp[1][x]; + int r = outbufp[2][x]; + b += g; r += g; + pb += b; + pg += g; + pr += r; + + *prevp += pb; fputc(*prevp, output); prevp++; + *prevp += pg; fputc(*prevp, output); prevp++; + *prevp += pr; fputc(*prevp, output); prevp++; + } + outbufp[0] += header.width; + outbufp[1] += header.width; + outbufp[2] += header.width; + break; + + case 4: + for(pr = 0, pg = 0, pb = 0, pa = 0, x = 0; + x < header.width; x++) + { + int b = outbufp[0][x]; + int g = outbufp[1][x]; + int r = outbufp[2][x]; + int a = outbufp[3][x]; + b += g; r += g; + pb += b; + pg += g; + pr += r; + pa += a; + *prevp += pb; fputc(*prevp, output); prevp++; + *prevp += pg; fputc(*prevp, output); prevp++; + *prevp += pr; fputc(*prevp, output); prevp++; + *prevp += pa; fputc(*prevp, output); prevp++; + } + outbufp[0] += header.width; + outbufp[1] += header.width; + outbufp[2] += header.width; + outbufp[3] += header.width; + break; + } + } + } +} diff --git a/UI/build.xml b/UI/build.xml new file mode 100644 index 0000000..a7be753 --- /dev/null +++ b/UI/build.xml @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UI/lib/tcommon.jar b/UI/lib/tcommon.jar new file mode 100644 index 0000000..057fc56 Binary files /dev/null and b/UI/lib/tcommon.jar differ diff --git a/UI/src/nl/weeaboo/krkr/InvalidLayeringException.java b/UI/src/nl/weeaboo/krkr/InvalidLayeringException.java new file mode 100644 index 0000000..77ba31a --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/InvalidLayeringException.java @@ -0,0 +1,12 @@ +package nl.weeaboo.krkr; + +@SuppressWarnings("serial") +public class InvalidLayeringException extends RuntimeException { + + //Functions + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/KiriKiriConverter.java b/UI/src/nl/weeaboo/krkr/KiriKiriConverter.java new file mode 100644 index 0000000..54d7f00 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/KiriKiriConverter.java @@ -0,0 +1,365 @@ +package nl.weeaboo.krkr; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import nl.weeaboo.common.Dim; +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.settings.INIFile; +import nl.weeaboo.vnds.FileExts; +import nl.weeaboo.vnds.FileMapper; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.Patcher; + +public class KiriKiriConverter { + + private String sourceFileEncoding = "UTF-16"; + protected boolean showOutput = false; + protected boolean ignoreText = false; + + //Patching + protected Patcher patcher; + protected Map appendMap; + protected Map> patchPreMap; + protected Map> patchPostMap; + + private File scriptFolder; + private File outFolder; + private File infoFolder; + + private int imageW, imageH; + protected MacroParser macroParser; + protected final FileExts fileExts; + + private FileMapper filenameMapper; + private Set scriptFilesWritten; + private Set unhandledTextMacros; + private Set unhandledMacros; + private List parseErrors; + private List layeringErrors; + + public KiriKiriConverter(File srcF, File scriptF, File dstF) { + this.scriptFolder = scriptF; + this.outFolder = new File(dstF, "script"); + this.infoFolder = new File(dstF, "_info"); + + Dim d = new Dim(256, 192); + try { + INIFile imgIni = new INIFile(); + imgIni.read(new File(dstF, "img.ini")); + d = new Dim(imgIni.getInt("width", d.w), + imgIni.getInt("height", d.h)); + } catch (IOException ioe) { + Log.w("Error reading img.ini", ioe); + } + imageW = d.w; + imageH = d.h; + + FileExts exts; + try { + exts = FileExts.fromFile(new File(dstF, "exts.ini")); + } catch (IOException e) { + //Ignore + exts = new FileExts(); + } + fileExts = exts; + + macroParser = new MacroParser(this); + } + public void convert() { + float scale = Math.min(256f/imageW, 192f/imageH); + macroParser.setScale(scale); + + outFolder.mkdirs(); + infoFolder.mkdirs(); + + appendMap = new HashMap(); + patchPreMap = new HashMap>(); + patchPostMap = new HashMap>(); + + patcher = createPatcher(); + patcher.patchPre(patchPreMap); + patcher.patchPost(patchPostMap); + patcher.fillAppendMap(appendMap); + + filenameMapper = new FileMapper(); + scriptFilesWritten = new TreeSet(); + unhandledTextMacros = new TreeSet(); + unhandledMacros = new TreeSet(); + parseErrors = new ArrayList(); + layeringErrors = new ArrayList(); + + try { + filenameMapper.load(infoFolder+"/filenames.txt"); + } catch (IOException e) { } + + Map scriptFolderContents = new TreeMap(); + FileUtil.collectFiles(scriptFolderContents, scriptFolder, false); + for (int n = 1; n <= getNumberOfPasses(); n++) { + for (Entry entry : scriptFolderContents.entrySet()) { + scriptConvert(n, entry.getKey(), entry.getValue()); + } + } + + //Generate script files for each file in the append map, so appending to a non-existing file + //generates a new file. + for (Entry entry : appendMap.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value == null) { + continue; + } + + if (!scriptFilesWritten.contains(new File(createOutputPath(key)))) { + try { + LinkedList list = new LinkedList(); + list.add(value); + writeScript(list, key); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + } + + try { filenameMapper.save(infoFolder+"/filenames.txt"); } catch (IOException e) { Log.w("Exception", e); } + try { saveStringSet(unhandledTextMacros, infoFolder+"/unhandled_textmacro.txt"); } catch (IOException e) { Log.w("Exception", e); } + try { saveStringSet(unhandledMacros, infoFolder+"/unhandled_macro.txt"); } catch (IOException e) { Log.w("Exception", e); } + try { saveStringSet(parseErrors, infoFolder+"/parse_errors.txt"); } catch (IOException e) { Log.w("Exception", e); } + try { saveStringSet(layeringErrors, infoFolder+"/layering_errors.txt"); } catch (IOException e) { Log.w("Exception", e); } + } + + //Functions + protected static void saveStringSet(Collection strings, String filename) throws IOException { + BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(filename), 512*1024); + try { + for (String s : strings) { + fout.write(s.getBytes("UTF-8")); + fout.write('\n'); + } + fout.flush(); + } finally { + fout.close(); + } + } + + protected Patcher createPatcher() { + return new Patcher(); + } + + protected void scriptConvert(int pass, String relpath, File file) { + if (pass == getNumberOfPasses() && file.getName().endsWith("ks")) { + try { + processKSFile(file, file.getName()); + } catch (IOException e) { + Log.w("Exception during script convert", e); + } + } + } + protected void processKSFile(File file, String patchMapKey) throws IOException { + Map patchPreMap = this.patchPreMap.remove(patchMapKey); + if (patchPreMap == null) patchPreMap = new HashMap(); + Map patchPostMap = this.patchPostMap.remove(patchMapKey); + if (patchPostMap == null) patchPostMap = new HashMap(); + + List list = new ArrayList(); + BufferedReader in = new BufferedReader(new InputStreamReader( + new FileInputStream(file), getSourceFileEncoding())); + + macroParser.reset(); + + String line; + int t = 1; + while ((line = in.readLine()) != null) { + if (patchPreMap.get(t) != null) { + line = patchPreMap.get(t); + } + + if (patchPostMap.get(t) != null) { + list.add(macroParser.flush(file.getName(), t, line)); + list.add(patchPostMap.get(t)); + } else if (line.startsWith("@")) { + //Macro + String val = macroParser.parseMacro(file.getName(), t, line); + if (val != null && val.length() > 0) { + list.add(val); + } + } else { + if (line.startsWith("*") || line.startsWith(";")) { + //Label or comment + list.add("#" + line); + } else { + //Text + String parsed = parseText(file.getName(), t, line); + if (parsed != null) { + list.add((ignoreText ? "#" : "") + parsed); + } + } + } + t++; + } + in.close(); + + String appendData = appendMap.remove(patchMapKey); + //System.out.println(file.getName() + " " + patchMapKey + " " + (appendData != null ? appendData.length() : 0)); + if (appendData != null) { + list.add(appendData); + } + + //Write to disc + writeScript(list, file.getName()); + } + + protected void writeScript(List list, String filename) throws IOException { + String path = createOutputPath(filename); + + File outFile = new File(path); + scriptFilesWritten.add(outFile); + outFile.getParentFile().mkdirs(); + + BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outFile), 512*1024); + for (String s : list) { + byte bytes[] = s.getBytes("UTF-8"); + if (bytes.length > 0 && bytes[0] != '#') { + int start = 0; + while (start < bytes.length) { + if (bytes[start] != ' ' || bytes[start] != '\t' || bytes[start] != '\n') { + break; + } + start++; + } + if (start < bytes.length) { + int end = bytes.length; + while (end > start) { + if (bytes[end-1] != ' ' || bytes[end-1] != '\t' || bytes[end-1] != '\n') { + break; + } + end--; + } + + if (end-start > 0) { + out.write(bytes, start, end-start); + out.write('\n'); + } + } + } + } + + out.flush(); + out.close(); + + if (showOutput) { + System.out.println("Writing: " + path); + } + } + + protected String parseText(String filename, int lineNumber, String line) { + //Save+Remove macros + int index = 0; + while ((index = line.indexOf('[')) >= 0) { + int index2 = index+1; + boolean inQuotes = false; + while (index2 < line.length()) { + if (line.charAt(index2) == '\"') { + inQuotes = !inQuotes; + } else if (line.charAt(index2) == '\\') { + index2++; + } else if (line.charAt(index2) == ']') { + if (!inQuotes) { + index2++; + break; + } + } + index2++; + } + + String macro = line.substring(index, index2); + String insert = macroParser.parseTextMacro(filename, lineNumber, macro); + + line = line.substring(0, index) + insert + line.substring(index + macro.length(), line.length()); + index += insert.length(); + } + + //line = line.replaceAll("\\[([^\\[])*?\\]", ""); + return line.trim(); + } + + public String addRes(String prefix, String filename) { + return filenameMapper.add(prefix+filename.toLowerCase()); + } + + protected String createOutputPath(String filename) { + return new File(outFolder, StringUtil.stripExtension(filename) + ".scr").getAbsolutePath(); + } + + public static String repeatString(String pattern, int times) { + StringBuilder sb = new StringBuilder(); + for (int n = 0; n < times; n++) { + sb.append(pattern); + } + return sb.toString(); + } + + public void addUnhandledTextMacro(String macro) { + unhandledTextMacros.add(macro); + } + public void addUnhandledMacro(String macro) { + unhandledMacros.add(macro); + } + public void addParseError(String errorString) { + parseErrors.add(errorString); + } + public void addLayeringError(String errorString) { + layeringErrors.add(errorString); + } + + //Getters + public String getSourceFileEncoding() { return sourceFileEncoding; } + public File getScriptFolder() { return scriptFolder; } + public File getOutputFolder() { return outFolder; } + public File getInfoFolder() { return infoFolder; } + public int getNumberOfPasses() { return 1; } + public int getImageW() { return imageW; } + public int getImageH() { return imageH; } + + //Setters + public void setSourceFileEncoding(String enc) { + this.sourceFileEncoding = enc; + } + + //Append Map + public String createJump(String... options) { + StringBuilder sb1 = new StringBuilder("choice "); + StringBuilder sb2 = new StringBuilder(); + + int t = 1; + for (String s : options) { + String part[] = s.split("\\|"); + if (t > 1) { + sb1.append("|"); + } + sb1.append(part[0]); + + sb2.append("if selected == " + t + "\n"); + sb2.append(" jump " + part[1] + "\n"); + sb2.append("fi\n"); + t++; + } + return sb1.toString() + "\n" + sb2.toString(); + } +} diff --git a/UI/src/nl/weeaboo/krkr/MacroHandler.java b/UI/src/nl/weeaboo/krkr/MacroHandler.java new file mode 100644 index 0000000..2eaa8cd --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/MacroHandler.java @@ -0,0 +1,60 @@ +package nl.weeaboo.krkr; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + + +public class MacroHandler { + + protected KiriKiriConverter krkr; + protected MacroParser mp; + protected Set macroIgnoreList; + + public MacroHandler(KiriKiriConverter krkr, MacroParser mp) { + this.krkr = krkr; + this.mp = mp; + + macroIgnoreList = new HashSet(); + } + + //Functions + public void reset() { + } + public String flush() { + return ""; + } + + protected void ignore(String macro) { + macroIgnoreList.add(macro); + } + + public String process(String macro, Map params) throws IOException { + if (macroIgnoreList.contains(macro)) { + return ""; + } + return null; + } + + protected String macroToString(String macro, Map params) { + StringBuilder sb = new StringBuilder(macro); + for (Entry entry : params.entrySet()) { + sb.append(" " + entry.getKey() + "=" + entry.getValue()); + } + return sb.toString(); + } + + public int readInt(String s, int defaultValue) { + try { + return Integer.parseInt(s); + } catch (Exception e) { } + return defaultValue; + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/MacroParser.java b/UI/src/nl/weeaboo/krkr/MacroParser.java new file mode 100644 index 0000000..0dd157b --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/MacroParser.java @@ -0,0 +1,361 @@ +package nl.weeaboo.krkr; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.imageio.ImageIO; + +import nl.weeaboo.collections.Tuple2; +import nl.weeaboo.vnds.Log; + +public class MacroParser { + + private static final int maxImageCacheSize = 8; + private boolean disableSprites = false; + + public static String R_FOREGROUND = "foreground/"; + public static String R_BACKGROUND = "background/"; + public static String R_SOUND = "sound/"; + + protected KiriKiriConverter krkr; + protected float scale; + protected List macroHandlers; + protected List textMacroHandlers; + + protected boolean checkForLayeringErrors = true; + protected boolean blackedOut; + protected String currentBG; + protected Sprite slots[] = new Sprite[10]; + + private List> imageCache; + + public MacroParser(KiriKiriConverter krkr) { + this.krkr = krkr; + + scale = 1f; + macroHandlers = new ArrayList(); + textMacroHandlers = new ArrayList(); + imageCache = new LinkedList>(); + } + + //Functions + public void addMacroHandler(MacroHandler mh) { + macroHandlers.add(mh); + } + public void addTextMacroHandler(MacroHandler mh) { + textMacroHandlers.add(mh); + } + + public String flush(String filename, int lineNumber, String line) { + List lines = new ArrayList(); + try { + for (MacroHandler mh : macroHandlers) { + String s = mh.flush(); + if (s != null && s.length() > 0) lines.add(s); + } + } catch (InvalidLayeringException ile) { + krkr.addLayeringError(String.format("%s:%d ## %s", filename, lineNumber, line)); + } + + StringBuilder sb = new StringBuilder(); + for (String s : lines) { + if (sb.length() > 0) { + sb.append("\n"); + } + sb.append(s); + } + return sb.toString(); + } + + public void reset() { + blackedOut = false; + currentBG = "special/blackout.jpg"; + imageCache.clear(); + clearSlots(); + + for (MacroHandler mh : macroHandlers) { + mh.reset(); + } + for (MacroHandler mh : textMacroHandlers) { + mh.reset(); + } + } + + public void clearSlots() { + for (int n = 0; n < slots.length; n++) { + slots[n] = null; + } + } + + public String restoreSlots(Sprite ss[]) { + return restoreSlots(currentBG, ss); + } + public String restoreSlots(String newBG, final Sprite ss[]) { + boolean clash = false; //Is it necessary to clear the screen? + + /* + for (int n = 0; n < slots.length; n++) { + if (slots[n] != null) { + if (ss[n] == null || !slots[n].equals(ss[n])) { + clash = true; + break; + } + } else { + if (ss[n] != null) { + //If this sprite should be drawn underneath another sprite -> clash = true + for (int i = 0; i < slots.length; i++) { + //If should + if (slots[i] != null && ss[n].z < slots[i].z) { + //Check if ranges on the x-axis overlap + if ((slots[i].x >= ss[n].x && slots[i].x < ss[n].x + ss[n].w) + || (slots[i].x + slots[i].w >= ss[n].x && slots[i].x + slots[i].w < ss[n].x + ss[n].w)) + { + clash = true; + break; + } + } + } + } + } + } + */ + clash = true; + + String pre = (disableSprites ? "#" : ""); + String text = ""; + + if (!blackedOut) { + boolean hasNonNullSprites = false; + for (Sprite s : ss) { + if (s != null) { + hasNonNullSprites = true; + break; + } + } + + if (checkForLayeringErrors && currentBG.equals("special/blackout.jpg") + && hasNonNullSprites) + { + throw new InvalidLayeringException(); + } + + if (clash || !newBG.equals(currentBG)) { + currentBG = newBG; + text += pre + "bgload " + currentBG; + } + + List indexMapping = new ArrayList(); + for (int n = 0; n < ss.length; n++) { + indexMapping.add(n); + } + Collections.sort(indexMapping, new Comparator() { + public int compare(Integer i1, Integer i2) { + if (ss[i1] == null) { + return -1; + } else if (ss[i2] == null) { + return 1; + } + return (ss[i1].z > ss[i2].z ? 1 : (ss[i1].z == ss[i2].x ? 0 : -1)); + } + }); + + for (int x = 0; x < indexMapping.size(); x++) { + int n = indexMapping.get(x); + if (n < 0 || n >= slots.length) { + continue; + } + + if (clash || (ss[n] != null && !ss[n].equals(slots[n]))) { + Sprite s = ss[n]; + if (s != null) { + if (text.length() > 0) { + text += "\n"; + } + text += pre + "setimg " + s.image + " " + s.x + " " + s.y; + } + } + } + } else { + text = "#blackedout"; + } + + slots = ss; + + return text; + } + + public String parseTextMacro(String filename, int lineNumber, String macro) { + return parse(filename, lineNumber, 1, macro.substring(1, macro.length()-1)); + } + public String parseMacro(String filename, int lineNumber, String line) { + return parse(filename, lineNumber, 0, line.substring(1)); + } + protected String parse(String filename, int lineNumber, int parseType, String line) { + int index = line.indexOf(' '); + if (index < 0) index = line.length(); + + String macro = line.substring(0, index); + + Map params = new HashMap(); + + boolean inQuotes = false; + int mode = 0; + StringBuilder tempName = new StringBuilder(); + StringBuilder tempValue = new StringBuilder(); + for (int n = index + 1; n < line.length(); n++) { + char c = line.charAt(n); + + if (mode == 0) { + if (c == '=') { + mode = 1; + } else { + tempName.append(c); + } + } else if (mode == 1 && !Character.isWhitespace(c)) { + mode = 2; + } + if (mode == 2) { + if (inQuotes && c == '\"') { + inQuotes = false; + } + if (tempValue.length() == 0 && c == '\"') { + inQuotes = true; + } + + if (inQuotes || !Character.isWhitespace(c)) { + tempValue.append(c); + } + if ((!inQuotes && Character.isWhitespace(c)) || n >= line.length() - 1) { + String value = tempValue.toString().trim(); + if (value.charAt(0) == '\"') { + value = value.substring(1, value.length()-1); + } + + params.put(tempName.toString().trim(), value); + tempName.delete(0, tempName.length()); + tempValue.delete(0, tempValue.length()); + mode = 0; + inQuotes = false; + } + } + } + + Collection hs = (parseType == 1 ? textMacroHandlers : macroHandlers); + for (MacroHandler handler : hs) { + try { + String result = handler.process(macro, params); + if (result != null) { + while (parseType != 1 && result.endsWith("\n")) { + result = result.substring(0, result.length()-1); + } + return result; + } + } catch (InvalidLayeringException ile) { + krkr.addLayeringError(String.format("%s:%d ## %s", filename, lineNumber, line)); + return "#hidden by layering"; + } catch (Exception e) { + String error = String.format("Error Parsing line (%s:%d) ## %s :: %s", filename, lineNumber, line, e.toString()); + krkr.addParseError(error); + Log.w(error); + } + } + + if (parseType == 1) { + krkr.addUnhandledTextMacro(macro); + return ""; + } else { + krkr.addUnhandledMacro(macro); + return "#" + line; + } + } + + public Sprite createSprite(String oldName, String newName, int pos, int z) throws IOException { + oldName = oldName.toLowerCase(); + int x = 0; + int y = 0; + + BufferedImage image = null; + for (Tuple2 entry : imageCache) { + if (entry.x.equals(oldName)) { + image = entry.y; + break; + } + } + if (image == null) { + image = ImageIO.read(new File(krkr.getOutputFolder()+"/../foreground/"+oldName)); + while (imageCache.size() >= maxImageCacheSize) { + imageCache.remove(0); + } + imageCache.add(Tuple2.newTuple(oldName, image)); + } + + int iw = Math.round(scale * image.getWidth()); + int ih = (int)Math.floor(scale * image.getHeight()); + //System.out.println(scale + " " + image.getWidth() + "x" + image.getHeight() + " -> " + iw + "x" + ih); + + y = 192 - ih; + + if (pos == 0) { + x = (256 - iw) / 2; + } else if (pos == 1) { + x = 256*3/10 - iw/2; + } else if (pos == 2) { + x = 256*7/10 - iw/2; + } else if (pos == 3) { + x = 256/4 - iw/2; + } else if (pos == 4) { + x = 256*3/4 - iw/2; + } + + return new Sprite(x, y, z, newName, iw); + } + + public int parsePosValue(String value) { + int pos = 0; + + if (value.equals("rc") || value.equals("rightcenter")) { + pos = 2; + } else if (value.equals("lc") || value.equals("leftcenter")) { + pos = 1; + } else if (value.startsWith("r")) { + pos = 4; + } else if (value.startsWith("l")) { + pos = 3; + } else if (value.equals("all")) { + pos = -1; + } + return pos; + } + + //Getters + public String getCurrentBG() { return currentBG; } + public Sprite[] getSlotsCopy() { return slots.clone(); } + + //Setters + public String setBG(String s) { + if (currentBG != null && currentBG.equals(s)) { + return ""; + } + + blackedOut = false; + currentBG = s; + clearSlots(); + return "bgload " + s; + } + public void setBlackedOut(boolean b) { + blackedOut = b; + } + public void setScale(float s) { + scale = s; + } + +} diff --git a/UI/src/nl/weeaboo/krkr/Packer.java b/UI/src/nl/weeaboo/krkr/Packer.java new file mode 100644 index 0000000..6340431 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/Packer.java @@ -0,0 +1,200 @@ +package nl.weeaboo.krkr; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map.Entry; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipOutputStream; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.FileMapper; +import nl.weeaboo.vnds.Log; + +public class Packer { + + public Packer() { + } + + //Functions + + public void process(File currentFolder, File targetFolder) { + cleanAction(targetFolder); + targetFolder.mkdirs(); + + System.out.println("Copying files..."); + + copyAction(currentFolder, targetFolder); + + try { + FileMapper mapper = new FileMapper(); + mapper.load(currentFolder.getAbsolutePath()+"/_info/filenames.txt"); + modifyNameMapping(mapper); + + int t = 0; + for (Entry entry : mapper) { + if (processEntry(currentFolder, targetFolder, entry.getKey(), entry.getValue())) { + t++; + if ((t & 0xFF) == 0) { + System.out.printf("Files Copied: %d\n", t); + } + } + } + + System.out.println("Zipping files..."); + + zip(new File(targetFolder + "/foreground"), new File(targetFolder + "/foreground.zip")); + zip(new File(targetFolder + "/background"), new File(targetFolder + "/background.zip")); + zip(new File(targetFolder + "/script"), new File(targetFolder + "/script.zip")); + zip(new File(targetFolder + "/sound"), new File(targetFolder + "/sound.zip")); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + protected void modifyNameMapping(FileMapper mapper) { + } + + protected void cleanAction(File targetFolder) { + if (targetFolder.exists()) { + FileUtil.deleteFolder(targetFolder); + if (targetFolder.exists()) { + throw new RuntimeException("Unable to delete to target folder"); + } + } + } + + protected void copyAction(File currentFolder, File targetFolder) { + copy(new File(currentFolder + "/foreground/special"), new File(targetFolder.getAbsolutePath() + "/foreground/")); + copy(new File(currentFolder + "/background/special"), new File(targetFolder.getAbsolutePath() + "/background/")); + copy(new File(currentFolder + "/sound/special"), new File(targetFolder.getAbsolutePath() + "/sound/")); + copy(new File(currentFolder + "/script"), targetFolder); + + new File(targetFolder.getAbsolutePath() + "/save/").mkdirs(); + copy(new File(currentFolder + "/save"), targetFolder); + + copy(new File(currentFolder + "/default.ttf"), targetFolder); + copy(new File(currentFolder + "/icon.png"), targetFolder); + copy(new File(currentFolder + "/thumbnail.png"), targetFolder); + copy(new File(currentFolder + "/icon-high.png"), targetFolder); + copy(new File(currentFolder + "/thumbnail-high.png"), targetFolder); + copy(new File(currentFolder + "/icon-high.jpg"), targetFolder); + copy(new File(currentFolder + "/thumbnail-high.jpg"), targetFolder); + copy(new File(currentFolder + "/info.txt"), targetFolder); + + File imgIniF = new File(currentFolder + "/img.ini"); + if (imgIniF.exists()) { + copy(imgIniF, targetFolder); + } + } + + private boolean processEntry(File srcDir, File dstDir, String hash, String original) { + String relpath = original.substring(0, original.lastIndexOf('/')+1) + hash; + File src = new File(srcDir + File.separator + original); + File dst = new File(dstDir + File.separator + relpath); + if (src.exists()) { + try { + FileUtil.copyFile(src, dst); + } catch (IOException e) { + Log.w("Error copying file: " + src + " to " + dst, e); + } + } + return false; + } + + public static File copy(File src, File dstFolder) { + if (!src.exists()) { + return null; + } + + if (src.isDirectory()) { + for (String s : src.list()) { + copy(new File(src.getAbsolutePath() + File.separator + s), new File(dstFolder.getAbsolutePath() + File.separator + src.getName())); + } + return null; + } else { + File dst = new File(dstFolder.getAbsolutePath() + File.separator + src.getName()); + try { + FileUtil.copyFile(src, dst); + } catch (IOException e) { + Log.w("Error copying file: " + src + " to " + dst, e); + } + return dst; + } + } + + protected void zip(File folder, File zipFile) throws IOException { + zip(new File[] {folder}, zipFile); + } + protected void zip(File folders[], File zipFile) throws IOException { + for (File folder : folders) { + if (!folder.exists() || folder.listFiles().length == 0) { + continue; + } + + try { + ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(zipFile)); + zout.setMethod(ZipOutputStream.STORED); + + addToZip(zout, folder, "", true); + + zout.flush(); + zout.close(); + } catch (ZipException ze) { + Log.v("Empty ZIP file: " + zipFile); + zipFile.delete(); + } + } + } + protected void addToZip(ZipOutputStream zout, File file, String prefix, boolean deleteWhenDone) throws IOException { + addToZip(zout, file, prefix, file.getName(), deleteWhenDone); + } + protected void addToZip(ZipOutputStream zout, File file, String prefix, String filename, boolean deleteWhenDone) throws IOException { + if (!file.exists()) { + return; + } + + if (prefix.length() > 0) { + prefix += '/'; + } + prefix += filename; + + if (file.isDirectory()) { + for (File f : file.listFiles()) { + addToZip(zout, f, prefix, deleteWhenDone); + } + } else { + if (!filename.endsWith(".mp3")) { + //System.out.println("ZIP: " + file.getName()); + + //Read file contents and delete file afterwards + byte[] buffer = FileUtil.readBytes(file); + if (deleteWhenDone) { + file.delete(); + } + + //Create ZIP Entry + ZipEntry entry = new ZipEntry(prefix); + entry.setSize(buffer.length); + entry.setCompressedSize(buffer.length); + CRC32 crc = new CRC32(); + crc.update(buffer); + entry.setCrc(crc.getValue()); + zout.putNextEntry(entry); + + //Write File contents to ZIP + zout.write(buffer); + + zout.flush(); + zout.closeEntry(); + } + } + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/Sprite.java b/UI/src/nl/weeaboo/krkr/Sprite.java new file mode 100644 index 0000000..20d8a2b --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/Sprite.java @@ -0,0 +1,25 @@ +package nl.weeaboo.krkr; + +public class Sprite { + + public final int x; + public final int y; + public final int z; + public final String image; + public final int w; + + public Sprite(int x, int y, int z, String image, int w) { + this.x = x; + this.y = y; + this.z = z; + this.image = image; + this.w = w; + } + + public boolean equals(Object o) { + return (o instanceof Sprite ? equals((Sprite)o) : false); + } + public boolean equals(Sprite s) { + return s != null && s.image.equals(image) && s.x == x && s.y == y && s.z == z && s.w == w; + } +} diff --git a/UI/src/nl/weeaboo/krkr/XP3Extractor.java b/UI/src/nl/weeaboo/krkr/XP3Extractor.java new file mode 100644 index 0000000..2fe1701 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/XP3Extractor.java @@ -0,0 +1,343 @@ +package nl.weeaboo.krkr; + +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.ProgressListener; + +public class XP3Extractor { + + //Temporary buffers for unzip + private static final byte infBuffer[] = new byte[64 * 1024]; + private static final byte readBuffer[] = new byte[64 * 1024]; + + public XP3Extractor() { + } + + //Functions + public static final int read_s32(InputStream in) throws IOException { + return (int)readLE(in, 4); + } + public static final long read_s64(InputStream in) throws IOException { + return readLE(in, 8); + } + public static final long readLE(InputStream in, int bytes) throws IOException { + int result = 0; + for (int n = 0; n < bytes; n++) { + result += (in.read() << (8 * n)); + } + return result; + } + + /** + * Warning: dst should use ascii-only in its pathname + */ + public void extract(String archive, String dst, ProgressListener pl) throws IOException { + Log.v("Extracting " + archive); + + FileInputStream fin = new FileInputStream(archive); + FileChannel fc = fin.getChannel(); + + int origSize; + File uncompressedFile = new File(dst+"/__temp__.dat"); + uncompressedFile.getParentFile().mkdirs(); + + { + byte signature[] = new byte[] {(byte)'X', (byte)'P', (byte)'3', + (byte)0x0D, (byte)0x0A, (byte) ' ', (byte)0x0A, + (byte)0x1A, (byte)0x8B, (byte)0x67, (byte)0x01 }; + byte tempsig[] = new byte[signature.length]; + fin.read(tempsig); + for (int n = 0; n < tempsig.length; n++) { + if (signature[n] != tempsig[n]) { + throw new IOException("FileFormat error"); + } + } + + int indexOffset = (int)read_s64(fin); + if (indexOffset > fc.size()) throw new IOException("FileFormat error"); + fc.position(indexOffset); + + boolean compression = readLE(fin, 1) != 0; + if (compression) { + int compSize = (int)read_s64(fin); + origSize = (int)read_s64(fin); + + if (indexOffset+compSize+17 != fc.size()) throw new IOException("FileFormat error"); + + int uncompressedL = -1; + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(uncompressedFile)); + try { + uncompressedL = unzip(fin, bout, compSize); + } finally { + bout.close(); + } + + if (uncompressedL != origSize) throw new IOException("FileFormat error"); + } else { + origSize = (int)read_s64(fin); + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(uncompressedFile)); + int read = 0; + byte buffer[] = new byte[256*1024]; + while (read < origSize) { + int r = fin.read(buffer, 0, buffer.length); + if (r < 0) break; + bout.write(buffer, 0, r); + } + bout.close(); + } + } + + FileInputStream uncompressedIn = new FileInputStream(uncompressedFile); + FileChannel uncompressedC = uncompressedIn.getChannel(); + + byte out[] = new byte[1024 * 1024]; + int outL = 0; + + int t = 0; + int read = 0; + while (uncompressedC.position() < origSize) { + Entry entry = readEntry(uncompressedIn); + + File outFile = new File(String.format("%s/%s", dst, entry.file)); + outFile.getParentFile().mkdirs(); + + outL = 0; + t++; + + if (pl != null && (t & 0xFF) == 0) { + pl.onProgress(read, (int)fc.size(), ""); + } + + //Log.verbose("[write] " + outFile.getAbsolutePath()); + //Benchmark.tick(); + + //Write segments to seperate files + int totalSize = 0; + for (Segment segment : entry.segments) { + totalSize += segment.origSize; + } + if (out.length < totalSize) { + out = new byte[totalSize]; + } + + for (Segment segment : entry.segments) { + fc.position(segment.offset); + + if (segment.compressed) { + outL += unzip(fin, out, segment.compSize); + } else { + outL += fin.read(out, outL, segment.compSize); + } + + read += segment.compSize; + } + + //Decrypt + if (entry.encrypted) { + decrypt(outFile.getName(), out, outL); + } + + try { + if (outFile.getName().endsWith(".tlg")) { + if (out.length >= 2 && out[0] == 'B' && out[1] == 'M') { + //Bitmap with TLG extension, lolwut + File bmpF = new File(StringUtil.stripExtension(outFile.getAbsolutePath())+".bmp"); + FileUtil.writeBytes(bmpF, out, 0, outL); + } else { + /* + String tlgTemp = dst+"/__temp__.tlg"; + String bmpTemp = dst+"/__temp__.bmp"; + FileUtil.writeBytes(new File(tlgTemp), out, 0, outL); + Process p = ProcessUtil.execInDir( + String.format("tlg2bmp \"%s\" \"%s\"", + tlgTemp, bmpTemp), + "tools/"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + + outFile.delete(); + new File(tlgTemp).delete(); + new File(bmpTemp).renameTo(new File(StringUtil.stripExtension(outFile.getAbsolutePath())+".bmp")); + */ + + FileUtil.writeBytes(outFile, out, 0, outL); + } + } else { + FileUtil.writeBytes(outFile, out, 0, outL); + } + } catch (IOException ioe) { + if (outFile.getName().length() <= 128) { + //Don't warn about long (garbage?) filenames that may be used as padding + Log.w(ioe.toString()); + } + } + + //Benchmark.tock(outFile.getName() + " %s"); + } + + uncompressedC.close(); + uncompressedIn.close(); + + fc.close(); + fin.close(); + + uncompressedFile.delete(); + + if (pl != null) pl.onFinished(archive + " fully extracted"); + } + + static synchronized int unzip(InputStream in, byte out[], int inL) throws IOException { + Inflater inf = new Inflater(); + + int read = 0; + int inflated = 0; + try { + while (true) { + int i = inf.inflate(out, inflated, out.length-inflated); + if (i > 0) { + inflated += i; + } else if (inf.finished() || inf.needsDictionary()) { + return inflated; + } else { + int readLeft = readBuffer.length; + if (inL >= 0 && inL-read < readLeft) { + readLeft = inL-read; + } + int r = in.read(readBuffer, 0, readLeft); + if (r == -1) { + throw new EOFException("Unexpected end of ZLIB input stream"); + } + read += r; + inf.setInput(readBuffer, 0, r); + } + } + } catch (DataFormatException e) { + throw new IOException(e); + } + } + + static synchronized int unzip(InputStream in, OutputStream out, int inL) throws IOException { + Inflater inf = new Inflater(); + + int read = 0; + int inflated = 0; + try { + while (true) { + int i = inf.inflate(infBuffer, 0, infBuffer.length); + if (i > 0) { + inflated += i; + out.write(infBuffer, 0, i); + } else if (inf.finished() || inf.needsDictionary()) { + return inflated; + } else { + int readLeft = readBuffer.length; + if (inL >= 0 && inL-read < readLeft) { + readLeft = inL-read; + } + int r = in.read(readBuffer, 0, readLeft); + if (r == -1) { + throw new EOFException("Unexpected end of ZIP input stream"); + } + read += r; + inf.setInput(readBuffer, 0, r); + } + } + } catch (DataFormatException e) { + throw new IOException(e); + } + } + + @SuppressWarnings("unused") + protected Entry readEntry(InputStream in) throws IOException { + Entry entry = new Entry(); + + byte temp[] = new byte[4]; + + in.read(temp); + if (!new String(temp).equals("File")) throw new IOException("FileFormat error :: " + new String(temp)); + int entryLength = (int)read_s64(in); + + in.read(temp); + if (!new String(temp).equals("info")) throw new IOException("FileFormat error"); + int infoLength = (int)read_s64(in); + + entry.encrypted = read_s32(in) != 0; + int origSize = (int)read_s64(in); + int compSize = (int)read_s64(in); + + int filenameL = (int)readLE(in, 2); + //System.err.println(origSize + " " + compSize + " " + new String(temp) + " " + filenameL); + if (infoLength != filenameL*2+22) throw new IOException("FileFormat error"); + + char filename[] = new char[filenameL]; + for (int n = 0; n < filenameL; n++) { + filename[n] = (char)readLE(in, 2); + } + entry.file = new String(filename); + + in.read(temp); + if (!new String(temp).equals("segm")) throw new IOException("FileFormat error"); + int numSegments = ((int)read_s64(in)) / 28; + entry.segments = new Segment[numSegments]; + for (int n = 0; n < numSegments; n++) { + Segment s = new Segment(); + s.compressed = read_s32(in) != 0; + s.offset = (int)read_s64(in); + s.origSize = (int)read_s64(in); + s.compSize = (int)read_s64(in); + + entry.segments[n] = s; + } + + in.read(temp); + //System.err.println(new String(temp)); + if (read_s64(in) != 4) throw new IOException("FileFormat error"); + int adler = read_s32(in); + + return entry; + } + + public void decrypt(String filename, byte data[], int dataL) { + if (dataL > 0x13) { + data[0x13] ^= 1; + } + if (dataL > 0x2ea29) { + data[0x2ea29] ^= 3; + } + + for (int n = 0; n < dataL; n++) { + data[n] ^= 0x36; + } + } + + //Getters + + //Setters + + //Inner Classes + private static class Entry { + public String file; + public boolean encrypted; + public Segment segments[]; + } + private static class Segment { + public boolean compressed; + public int offset; + public int origSize; + public int compSize; + } + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/ConversionGUI.java b/UI/src/nl/weeaboo/krkr/fate/ConversionGUI.java new file mode 100644 index 0000000..892f092 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/ConversionGUI.java @@ -0,0 +1,118 @@ +package nl.weeaboo.krkr.fate; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import nl.weeaboo.awt.AwtUtil; +import nl.weeaboo.awt.FileBrowseField; +import nl.weeaboo.common.Dim; +import nl.weeaboo.krkr.fate.FateScriptConverter.Language; +import nl.weeaboo.vnds.AbstractConversionGUI; + +@SuppressWarnings("serial") +public class ConversionGUI extends AbstractConversionGUI { + + private static final String version = "1.2.5"; + + /* + * Changes: + * + * 2013/03/19 -- v1.2.5 + * - Colored background effects didn't scale to higher resolutions properly. + * + * 2012/12/08 -- v1.2.4 + * - Sprite scaling was incorrect + * + * 2012/04/29 -- v1.2.3 + * - Fixed Heaven's Feel unlock flag not getting set + * + * 2011/09/22 -- v1.2.2 + * - Dumb typo + * + * 2011/09/22 -- v1.2.1 + * - Android conversion uses Ogg-Vorbis for all audio + * - Support for pre-installed voice data without using a Realta Nua disc + * + * 2011/04/03 -- v1.2.0 + * - Support for Android and high-res output + */ + + protected final FileBrowseField realtaNuaField; + protected final JComboBox languageCombo; + + private Language lang; + + public ConversionGUI() { + super("Fate/Stay Night -> VNDS Conversion GUI v" + version, + ConversionGUI.class.getResource("res/icon.png"), + new File(""), + new File(""), + "fate", + true, + new Dim(800, 600)); + + realtaNuaField = FileBrowseField.writeFolder("", new File("")); + + languageCombo = new JComboBox(Language.values()); + languageCombo.setSelectedItem(Language.EN); + } + + public static void main(String args[]) { + AwtUtil.setDefaultLAF(); + System.setProperty("line.separator", "\n"); + + new ConversionGUI().create(); + } + + @Override + protected void fillPathsPanel(JPanel panel) { + super.fillPathsPanel(panel); + + panel.add(new JLabel("Realta Nua (Optional)")); panel.add(realtaNuaField); + } + + @Override + protected void fillSettingsPanel(JPanel panel) { + panel.add(new JLabel("Language")); panel.add(languageCombo); + + super.fillSettingsPanel(panel); + } + + @Override + protected boolean preConvertCheck(File gameFolder, File outputFolder) { + lang = (Language)languageCombo.getSelectedItem(); + + return super.preConvertCheck(gameFolder, outputFolder); + } + + @Override + protected void callResourceConverter(String templateFolder, String srcFolder, + String dstFolder, String... args) + { + String[] merged = new String[args.length+2]; + merged[0] = srcFolder; + merged[1] = dstFolder; + System.arraycopy(args, 0, merged, merged.length-args.length, args.length); + FateResourceConverter.main(merged); + } + + @Override + protected void callScriptConverter(String srcFolder, String dstFolder) { + List list = new ArrayList(); + list.add(srcFolder); + list.add(dstFolder); + list.add(lang.name()); + FateScriptConverter.main(list.toArray(new String[0])); + } + + @Override + protected void callPacker(String srcFolder, String dstFolder) { + FatePacker.main(new String[] {srcFolder, dstFolder}); + } + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateDirectoryFlattener.java b/UI/src/nl/weeaboo/krkr/fate/FateDirectoryFlattener.java new file mode 100644 index 0000000..8fbadc8 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateDirectoryFlattener.java @@ -0,0 +1,58 @@ +package nl.weeaboo.krkr.fate; + +import java.io.File; + +public class FateDirectoryFlattener { + + private static final File root = new File("c:/users/timon/desktop/2/"); + + //Functions + public static void main(String args[]) { + process(1, root); + process(2, root); + } + + public static void process(int pass, File file) { + if (file.isDirectory()) { + File files[] = file.listFiles(); + for (File f : files) { + process(pass, f); + } + + if (pass == 2) { + file.delete(); + } + } else { + if (pass == 1) { + String path = file.getAbsolutePath(); + path = path.substring(root.getAbsolutePath().length(), path.length()); + String filename = path.replace(File.separatorChar, '/').replaceAll("\\/", ""); + + //Uncomment when flattening the scenario folder, this fixes the garbled filenames + /*try { + //String newName = new String(filename.getBytes("SJIS"), "CP1252"); + String newName = new String(filename.getBytes("CP1252"), "CP1252"); + newName = newName.replace((char)0xfffd, '?'); + //newName = newName.replaceAll("\\?", ""); + String converted = RouteParser.scenarioFileRename(4, newName); + if (converted != null) { + filename = converted; + } else { + System.out.println(filename + " " + newName); + } + } catch (Exception e) { + e.printStackTrace(); + }*/ + + File f = new File(root.getAbsolutePath() + File.separator + filename); + file.renameTo(f); + System.out.println(file.getAbsolutePath() + " " + f.getAbsolutePath()); + } + } + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateExtractor.java b/UI/src/nl/weeaboo/krkr/fate/FateExtractor.java new file mode 100644 index 0000000..57b0e4d --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateExtractor.java @@ -0,0 +1,210 @@ +package nl.weeaboo.krkr.fate; + +import java.io.File; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedSet; +import java.util.TreeSet; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.krkr.XP3Extractor; +import nl.weeaboo.string.StringUtil2; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.ProgressListener; + +public class FateExtractor { + + private String realtaNuaPath; + + public FateExtractor() { + + } + + //Functions + protected static void printUsage() { + System.err.println("Usage: java -jar FateExtractor.jar \nflags:" + + "\n\t-rn " + + "\n\t-novoice" + + "\n\t-cleanTempFiles" + ); + } + + public static void main(String args[]) { + if (args.length < 2) { + printUsage(); + return; + } + + boolean extractVoice = true; + boolean cleanTempFiles = false; + FateExtractor ex = new FateExtractor(); + try { + for (int n = 2; n < args.length; n++) { + if (args[n].startsWith("-rn")) { + ex.realtaNuaPath = args[++n]; + } else if (args[n].startsWith("-novoice")) { + extractVoice = false; + } else if (args[n].startsWith("-cleanTempFiles")) { + cleanTempFiles = true; + } + } + } catch (RuntimeException re) { + printUsage(); + return; + } + + SortedSet archives = new TreeSet(StringUtil2.getStringComparator()); + + File src = new File(args[0]); + File dst = new File(args[1]); + if (src.isDirectory()) { + for (File file : src.listFiles()) { + if ("xp3".equalsIgnoreCase(StringUtil.getExtension(file.getName()))) { + if (file.getName().equals("patch6.xp3")) { + if (extractVoice && + (ex.realtaNuaPath == null || !new File(ex.realtaNuaPath).exists())) + { + archives.add(file.getAbsolutePath()); + } + } else { + archives.add(file.getAbsolutePath()); + } + } + } + } else { + archives.add(src.getAbsolutePath()); + } + + Log.v("Extracting..."); + //Extract .xp3 to outFolder/ + for (String arc : archives) { + ProgressListener pl = new ProgressListener() { + @Override + public void onProgress(int value, int max, String message) { + Log.v(String.format("%s/%s %s...", StringUtil.formatMemoryAmount(value), + StringUtil.formatMemoryAmount(max), message)); + } + @Override + public void onFinished(String message) { + Log.v(message); + } + }; + + try { + ex.extractXP3(arc, dst.getAbsolutePath(), pl); + } catch (IOException e) { + Log.e("Exception extracting XP3 archive", e); + } + } + + if (ex.realtaNuaPath != null && extractVoice) { + RealtaNuaSoundExtractor rne = new RealtaNuaSoundExtractor(); + try { + rne.extract(ex.realtaNuaPath, dst.getAbsolutePath()+"/patch6"); + } catch (IOException e) { + Log.w("Unable to extract voice data from Realta Nua: " + e); + } + } + + Log.v("Flattening Folders..."); + for (File folder : dst.listFiles()) { + if (folder.isDirectory()) { + Map fileMap = new Hashtable(); + FileUtil.collectFiles(fileMap, folder, false); + + for (Entry entry : fileMap.entrySet()) { + File file = entry.getValue(); + File target = new File(folder.getAbsolutePath()+'/'+file.getName().toLowerCase()); + + if (!file.equals(target) && + !file.getAbsolutePath().equalsIgnoreCase(target.getAbsolutePath())) + { + target.delete(); + } + file.renameTo(target); + } + + FileUtil.deleteEmptyFolders(folder); + } + } + + Log.v("Patching..."); + + //Create patch list + SortedSet patchFolders = new TreeSet(StringUtil2.getStringComparator()); + for (File folder : dst.listFiles()) { + if (folder.isDirectory() && folder.getName().toLowerCase().startsWith("patch")) { + patchFolders.add(folder.getAbsolutePath()); + } + } + + Map patchMap = new Hashtable(); + for (String folder : patchFolders) { + //Iterate the sorted list of patches so patch2 gets overwritten by patch6 + FileUtil.collectFiles(patchMap, new File(folder), false); + } + + //Apply patches to each archive separately + for (File folder : dst.listFiles()) { + if (folder.isDirectory() && !folder.getName().toLowerCase().startsWith("patch")) { + Map fileMap = new Hashtable(); + FileUtil.collectFiles(fileMap, folder, false); + + for (Entry patchEntry : patchMap.entrySet()) { + File file = fileMap.get(patchEntry.getKey()); + if (file != null) { + //Overwrite if exists + try { + FileUtil.copyFile(patchEntry.getValue(), file); + } catch (IOException e) { + Log.w("Error trying to copy file: " + patchEntry.getValue() + " to " + file, e); + } + } + } + } + } + + if (cleanTempFiles) { + Log.v("Cleaning temp files..."); + + String delete[] = new String[] {"image", "patch", "patch2", "patch3", "patch4", "patch5", + "patch", "system", "version", "video"}; + for (String d : delete) { + FileUtil.deleteFolder(new File(dst.getAbsolutePath()+'/'+d)); + } + } + Log.v("Done."); + } + + public void extractXP3(String archive, String outFolder, ProgressListener pl) throws IOException { + XP3Extractor xp3ex = new XP3Extractor(); + xp3ex.extract(archive, outFolder+"/"+StringUtil.stripExtension(archive.substring(archive.replace('\\', '/').lastIndexOf('/'))), pl); + + /* + if (archive.endsWith("patch6.xp3")) { + //Weird, uncompressed xp3, crass doesn't like that + XP3Extractor xp3ex = new XP3Extractor(); + xp3ex.extract(archive, outFolder+"/"+FileUtil.stripExtension(archive.substring(archive.replace('\\', '/').lastIndexOf('/')))); + return; + } + + ProcessOutputReader pr = new ProcessOutputReader(); + String cmd = String.format("crage -p \"%s\" -o \"%s\" -O game=FSN", archive, outFolder); + System.out.println(cmd); + Process p = SystemUtil.execInDir(cmd, "tools/crass-0.4.13.0"); + + String output = pr.read(p).trim(); + if (output.length() > 0) { + System.err.println(output); + } + */ + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateInstaller.java b/UI/src/nl/weeaboo/krkr/fate/FateInstaller.java new file mode 100644 index 0000000..c08b6ef --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateInstaller.java @@ -0,0 +1,177 @@ +package nl.weeaboo.krkr.fate; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.awt.AwtUtil; +import nl.weeaboo.awt.DirectoryChooser; +import nl.weeaboo.awt.Sash; +import nl.weeaboo.vnds.ProgressListener; +import nl.weeaboo.vnds.ProgressRunnable; +import nl.weeaboo.vnds.VNDSProgressDialog; +import nl.weeaboo.vnds.installer.Installer; + +/* + * Changes: + * + * 2009/03/01 -- v1.1 + * - Moved xml config files to a subfolder + * + * 2008/11/03 -- v1.0 + * - Initial Release + * + */ +@SuppressWarnings("serial") +public class FateInstaller extends JFrame { + + private ComponentCheckBox coreCheck; + private ComponentCheckBox prologueCheck; + private ComponentCheckBox route1Check; + private ComponentCheckBox route2Check; + private ComponentCheckBox route3Check; + + public FateInstaller() { + setTitle("Fate/Stay Night Installer v1.1"); + + add(createCenterPanel()); + + pack(); + setResizable(false); + setLocationRelativeTo(null); + setVisible(true); + } + + //Functions + public static void main(String args[]) { + AwtUtil.setDefaultLAF(); + + FateInstaller fi = new FateInstaller(); + fi.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + + private JPanel createCenterPanel() { + coreCheck = new ComponentCheckBox("Core", "_installer/core.xml"); + coreCheck.setEnabled(false); + prologueCheck = new ComponentCheckBox("Prologue", "_installer/prologue.xml"); + route1Check = new ComponentCheckBox("Route 1: Fate", "_installer/route01-fate.xml"); + route2Check = new ComponentCheckBox("Route 2: UBW", "_installer/route02-ubw.xml"); + route3Check = new ComponentCheckBox("Route 3: HF", "_installer/route03-hf.xml"); + + JPanel panel2 = new JPanel(new GridLayout(-1, 1, 5, 5)); + panel2.add(coreCheck); + panel2.add(prologueCheck); + panel2.add(route1Check); + panel2.add(route2Check); + panel2.add(route3Check); + + JButton installButton = new JButton("Install"); + installButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + DirectoryChooser dc = new DirectoryChooser(true); + if (!dc.showDialog(FateInstaller.this, "Choose a folder to install to...")) { + return; + } + final String installFolder = dc.getSelectedDirectory().getAbsolutePath()+"/fate/"; + final List files = new ArrayList(); + + if (coreCheck.isSelected()) files.add(coreCheck.getPath()); + if (prologueCheck.isSelected()) files.add(prologueCheck.getPath()); + if (route1Check.isSelected()) files.add(route1Check.getPath()); + if (route2Check.isSelected()) files.add(route2Check.getPath()); + if (route3Check.isSelected()) files.add(route3Check.getPath()); + + ProgressListener pl = new ProgressListener() { + public void onFinished(String message) { + JOptionPane.showMessageDialog(null, String.format( + "Installation finished.
Installed to: %s", installFolder), + "Finished", JOptionPane.PLAIN_MESSAGE); + } + public void onProgress(int value, int max, String message) { + } + }; + + ProgressRunnable task = new ProgressRunnable() { + public void run(ProgressListener pl) { + final String errors = Installer.install(installFolder, pl, files.toArray(new String[0])); + + if (errors.trim().length() > 0) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + JOptionPane.showMessageDialog(null, + String.format("%s", errors.replaceAll("\\\n", "
")), + "Error", JOptionPane.ERROR_MESSAGE); + } + }); + } + } + }; + VNDSProgressDialog dialog = new VNDSProgressDialog(); + dialog.showDialog(task, pl); + } + }); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + buttonPanel.add(installButton); + + JPanel bottomPanel = new JPanel(new BorderLayout(10, 10)); + bottomPanel.add(new Sash(Sash.HORIZONTAL), BorderLayout.NORTH); + bottomPanel.add(buttonPanel, BorderLayout.CENTER); + + JPanel panel = new JPanel(new BorderLayout(10, 10)); + panel.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel.add(panel2, BorderLayout.CENTER); + panel.add(bottomPanel, BorderLayout.SOUTH); + return panel; + } + + //Getters + + //Setters + + //Inner Classes + private static class ComponentCheckBox extends JPanel { + + private String path; + private JCheckBox check; + + public ComponentCheckBox(String labelString, String path) { + this.path = path; + + JLabel label = new JLabel(labelString); + label.setPreferredSize(new Dimension(100, 20)); + check = new JCheckBox(); + check.setSelected(true); + + setLayout(new BorderLayout(10, 0)); + add(label, BorderLayout.WEST); + add(check, BorderLayout.CENTER); + } + + public boolean isSelected() { + return check.isSelected(); + } + public String getPath() { + return path; + } + + public void setEnabled(boolean e) { + super.setEnabled(e); + check.setEnabled(e); + } + } +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateMacroHandler.java b/UI/src/nl/weeaboo/krkr/fate/FateMacroHandler.java new file mode 100644 index 0000000..8430ed5 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateMacroHandler.java @@ -0,0 +1,377 @@ +package nl.weeaboo.krkr.fate; + +import static nl.weeaboo.krkr.MacroParser.R_BACKGROUND; +import static nl.weeaboo.krkr.MacroParser.R_FOREGROUND; +import static nl.weeaboo.krkr.MacroParser.R_SOUND; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import nl.weeaboo.krkr.MacroHandler; +import nl.weeaboo.krkr.MacroParser; +import nl.weeaboo.krkr.Sprite; +import nl.weeaboo.vnds.FileExts; + +public class FateMacroHandler extends MacroHandler { + + public static final int DEFAULT_Z = 1000; + + protected final FileExts fileExts; + + protected FateScriptConverter fc; + protected Sprite sprites[]; + protected boolean spriteFlushNeeded; + protected int soundPlayingLength; + + public FateMacroHandler(FateScriptConverter fc, MacroParser mp, FileExts exts) { + super(fc, mp); + + this.fc = fc; + this.fileExts = exts; + + String ignore[] = new String[] { + //Inline + "aero", "atlas", "keraino", "troya", "margos", "heart", + "l", "r", + + //Normal + "backlay", "broadencombo", "broadencomboT", + "canseeStatusMenu", "canSeeStatusMenu", + "cinescoT", "cinesco_offT", "clickskip", + "cm", //Called on the start of each day + "condoff", "condoffT", + "contrast", "contrastT", "contrastoff", "contrastoffT", + "darken", "darkenoff", "darkenT", "darkenoffT", + "dash", "dashcombo", "dashcomboT", + "defocus", "delay", "displayedoff", "displayedon", + "encountServant", "erasestaffroll", + "flicker", "flickerT", "flushcombo", "foldcombo", "foldcomboT", + "font", "haze", "hazeTrans", "hazetrans", "hearttonecombo", + "image", "imageex", "image4demo", //Used for animations only + "initabsolute", //??? + "interlude_end", "interlude_in", "interlude_in_", "interlude_out", "interlude_out_", "interlude_start", + "knowMasterName", "knowTrueName", "large", "layopt", + "monocro", "monocroT", "move", "nega", "negaT", + "nohaze_next", + "noise", "noise_back", "noise_noback", "noiseT", "stopnoise", "stopnoiseT", //Overlays animated white noise on the background + /*"pasttime", "pasttime_long",*/ //What do these do? <-- Some kind of background transition it seems + "pgnl", "pgtg", "prickT", "quad", + "quake", "quake_max", "quakeT", + "r", //Inserts a newline + "rclick", "redraw", "resetfont", "resetwait", "return", "rf", + "sepia", "sepiaT", "shock", "shockT", + "slideclosecomboT", /*"slideopencomboT",*/ "small", + "smudge", "smudgeT", "smudgeoff", "smudgeoffT", + "splinemovecombo", "splinemovecomboT", "staffrollsetting", + "stophaze", "superpose", "superpose_off", "textoff", "texton", + "tiger_start", "tiger_end", //Change font-style etc. to tiger-dojo style and back + "touchimages", "trans", "transex_w", /*"turnaround",*/ "tvoffcomboT", "useSkill", + "useSpecial", "useWeapon", + "wait", "waitT", "waitn", + "waveT", "whaze", "wm", "wq", "wshock", "wstaffroll", "wt", "zoomming" + }; + + for (String s : ignore) { + ignore(s); + } + } + + //Functions + public void reset() { + sprites = mp.getSlotsCopy(); + spriteFlushNeeded = false; + } + + public String process(String macro, Map params) throws IOException { + StringBuilder result = new StringBuilder(); + + if (macro.equals("pg")) { + if (fc.isSoundPlaying()) { + result.append("sound ~\n"); + fc.setSoundPlaying(false); + } + } + + if (macro.startsWith("ldall")) { + result.append(parse_ldall(params)); + } else if (macro.equals("ld") || macro.startsWith("ld_")) { + result.append(parse_ld(params)); + } else if (macro.equals("cl") || macro.startsWith("cl_")) { + result.append(parse_cl(params)); + } else { + mp.setBlackedOut(false); + + if (macro.startsWith("i2") || macro.startsWith("a2") || macro.equals("bg")) { + result.append(parse_bg(params)); + } else if (macro.equals("rep")) { + result.append(parse_rep(params)); + } else if (macro.equals("fadein")) { + result.append(parse_fadein(params)); + } else if (macro.equals("flushover")) { + result.append(parse_flushover(params)); + } else if (macro.equals("black")) { + result.append(parse_black(params)); + } else if (macro.startsWith("pasttime")) { + result.append(parse_pasttime(params)); + } else if (macro.equals("blackout")) { + result.append(parse_blackout(params)); + } else if (macro.equals("blue") || macro.equals("blueT")) { + result.append(parse_blue(params)); + } else if (macro.equals("red") || macro.equals("redT")) { + result.append(parse_red(params)); + } else if (macro.equals("white") || macro.equals("whiteT")) { + result.append(parse_white(params)); + } else if (macro.equals("green") || macro.equals("greenT")) { + result.append(parse_green(params)); + } else { + //Flush sprites whenever a non-sprite, non-bg command is issued. + + result.append(flush()); + if (result.length() > 0) result.append("\n"); + } + } + + if (macro.equals("date_title")) { + result.append(parse_date_title(params)); + } else if (macro.equals("l")) { + result.append("text "); + } else if (macro.equals("edoublecolumn")) { + result.append(parse_edoublecolumn(params)); + } else if (macro.equals("approachTigerSchool")) { + result.append(parse_approachTigerSchool(params)); + } else if (macro.equals("slideopencomboT")) { + result.append(parse_slideopencomboT(params)); + } else if (macro.equals("turnaround")) { + result.append(parse_turnaround(params)); + } else if (macro.startsWith("hearttonecombo")) { + Map p = new HashMap(); + p.put("file", "se028"); + result.append(process("se", p)); + } + + //Audio Functions + if (macro.startsWith("playstop") || macro.startsWith("playpause") || macro.startsWith("playresume")) { + result.append("music ~"); + } else if (macro.equals("play") || macro.equals("play_")) { + String filename = krkr.addRes(R_SOUND, params.get("file")+"."+fileExts.music); + result.append("music " + filename); + } else if (macro.startsWith("seloop")) { + String filename = krkr.addRes(R_SOUND, params.get("file")+"."+fileExts.sound); + + //Multiple concurrent music streams aren't supported (yet) + //return "music " + filename; + result.append("sound ~\nsound " + filename); + } else if (macro.equals("se") || macro.equals("se_")) { + String filename = krkr.addRes(R_SOUND, params.get("file")+"."+fileExts.sound); + result.append(playSFX(params, params.get("file"), filename)); + } else if (macro.startsWith("sestop")) { + result.append("sound ~"); + } else if (macro.equals("say") || macro.equals("lvoice")) { //@say in EN, JA, @lvoice in CH + String fp = (macro.equals("say") ? params.get("n") : params.get("file")); + + if (macro.equals("say")) { + String filename = krkr.addRes(MacroParser.R_SOUND, fp + "." + fileExts.voice); + result.append(String.format("sound %s", filename)); + } else { + //Don't check if the file exists in CH version + //Don't change the filename either + result.append(String.format("sound %s.%s", fp, fileExts.voice)); + } + fc.setSoundPlaying(true); + } + + if (result.length() > 0) { + return result.toString(); + } + + return super.process(macro, params); + } + + private String addSprite(int pos, Sprite sprite) { + sprites[pos] = sprite; + + spriteFlushNeeded = true; + return ""; + } + public String flush() { + if (spriteFlushNeeded) { + spriteFlushNeeded = false; + return mp.restoreSlots(sprites); + } + return ""; + } + private String setBG(String filename) { + clearSlots(); + return mp.setBG(filename); + } + private void clearSlots() { + mp.clearSlots(); + sprites = mp.getSlotsCopy(); + spriteFlushNeeded = false; + } + private String playSFX(Map params, String oldFilename, String newFilename) throws IOException { + File file = new File(krkr.getOutputFolder() + "/../sound/" + oldFilename + "." + fileExts.sound); + int waitTime = Math.min(60, Math.max(0, (int)Math.ceil(60f * (file.length()-4) / 11025f)-6)); + + int wait = (fc.isSoundPlaying() ? soundPlayingLength : 0); + soundPlayingLength = waitTime; + fc.setSoundPlaying(true); + return String.format("delay %d\nsound %s", wait, newFilename); + } + + private String parse_ldall(Map params) throws IOException { + clearSlots(); + spriteFlushNeeded = true; + + if (params.containsKey("l")) { + String name = params.get("l")+".png"; + String newName = krkr.addRes(R_FOREGROUND, name); + addSprite(3, mp.createSprite(name, newName, 3, readInt(params.get("il"), DEFAULT_Z))); + } + if (params.containsKey("lc")) { + String name = params.get("lc")+".png"; + String newName = krkr.addRes(R_FOREGROUND, name); + addSprite(1, mp.createSprite(name, newName, 1, readInt(params.get("ilc"), DEFAULT_Z))); + } + if (params.containsKey("c")) { + String name = params.get("c")+".png"; + String newName = krkr.addRes(R_FOREGROUND, name); + addSprite(0, mp.createSprite(name, newName, 0, readInt(params.get("ic"), DEFAULT_Z))); + } + if (params.containsKey("rc")) { + String name = params.get("rc")+".png"; + String newName = krkr.addRes(R_FOREGROUND, name); + addSprite(2, mp.createSprite(name, newName, 2, readInt(params.get("irc"), DEFAULT_Z))); + } + if (params.containsKey("r")) { + String name = params.get("r")+".png"; + String newName = krkr.addRes(R_FOREGROUND, name); + addSprite(4, mp.createSprite(name, newName, 4, readInt(params.get("ir"), DEFAULT_Z))); + } + + return ""; + } + private String parse_ld(Map params) throws IOException { + int pos = mp.parsePosValue(params.get("pos")); + + String oldFilename = params.get("file")+".png"; + String filename = krkr.addRes(R_FOREGROUND, oldFilename); + int z = readInt(params.get("index"), DEFAULT_Z); + + return addSprite(pos, mp.createSprite(oldFilename, filename, pos, z)); + } + private String parse_bg(Map params) { + String filename = params.get("file")+".jpg"; + filename = krkr.addRes(R_BACKGROUND, filename); + return setBG(filename); + } + private String parse_fadein(Map params) { + //I think it should clear all sprites, not 100% sure though + clearSlots(); + + String filename = krkr.addRes(R_BACKGROUND, params.get("file")+".jpg"); + return setBG(filename); + } + private String parse_rep(Map params) { + String filename = krkr.addRes(R_BACKGROUND, params.get("bg")+".jpg"); + return setBG(filename); + } + private String parse_cl(Map params) { + int pos = mp.parsePosValue(params.get("pos")); + + //If pos >= 0 delete 1 sprite, if < 0, delete them all + if (pos >= 0) { + sprites[pos] = null; + } else { + for (int n = 0; n < sprites.length; n++) { + sprites[n] = null; + } + } + spriteFlushNeeded = true; + + return ""; + } + private String parse_pasttime(Map params) { + clearSlots(); + return flash("special/blackout.jpg", 60 * 800 / 1000); + } + private String parse_flushover(Map params) { + return setBG("special/whiteout.jpg"); + } + private String parse_black(Map params) { + return setBG("special/blackout.jpg"); + } + private String parse_white(Map params) { + return parse_color_t("special/whiteout.jpg", params); + } + private String parse_red(Map params) { + return parse_color_t("special/redout.jpg", params); + } + private String parse_green(Map params) { + return parse_color_t("special/greenout.jpg", params); + } + private String parse_blue(Map params) { + return parse_color_t("special/blueout.jpg", params); + } + private String parse_color_t(String filename, Map params) { + if (params.get("time") != null) { + return flash(filename, Integer.parseInt(params.get("time")) * 60 / 1000); + } else { + return setBG(filename); + } + } + private String flash(String filename, int delay) { + String oldBG = mp.getCurrentBG(); + Sprite[] oldSlots = (sprites != null ? sprites.clone() : null); + + String result = setBG(filename) + "\ndelay " + delay; + String append = setBG(oldBG); + result += (append.length() > 0 ? "\n" : "") + append; + + sprites = oldSlots; + spriteFlushNeeded = true; + + return result; + } + + private String parse_blackout(Map params) { + mp.setBlackedOut(true); + return ""; + } + private String parse_date_title(Map params) { + String date = params.get("date"); + String month = date.substring(0, date.length()-2); + if (month.length() <= 2) month = "0" + month; + + String day = date.substring(date.length()-2, date.length()); + return "text ~\ntext \ntext ~"; + } + private String parse_edoublecolumn(Map params) { + String upper = params.get("upper").replaceAll("\\$", ""); + String lower = params.get("lower").replaceAll("\\$", ""); + + return "text " + upper + "\ntext " + lower; + } + private String parse_approachTigerSchool(Map params) { + return "bgload special/blackout.jpg\ntext \n" + + "bgload special/tigerdojo.jpg\ntext Welcome to Tiger Dojo."; + } + private String parse_slideopencomboT(Map params) { + String filename = params.get("nextimage"); + filename = krkr.addRes(R_BACKGROUND, filename+".jpg"); + return setBG(filename); + } + private String parse_turnaround(Map params) { + String bg = mp.getCurrentBG(); + String temp = setBG("special/blackout.jpg"); + clearSlots(); + return temp + "\n" + setBG(bg); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FatePacker.java b/UI/src/nl/weeaboo/krkr/fate/FatePacker.java new file mode 100644 index 0000000..413e1bf --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FatePacker.java @@ -0,0 +1,150 @@ +package nl.weeaboo.krkr.fate; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.krkr.Packer; +import nl.weeaboo.krkr.fate.FateScriptConverter.Language; +import nl.weeaboo.vnds.FileMapper; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.installer.InstallerPacker; + +public class FatePacker extends Packer { + + private String language; + + public FatePacker(String language, String in) { + this.language = language; + } + + //Functions + protected static void printUsage() { + System.err.println("Usage: java -jar FatePacker.jar \nflags:" + + "\n\t-novoice" + + "\n\t-threads " + + "\n\t-lang " + + "\n\t-cleanTempFiles"); + } + + public static void main(String args[]) { + if (args.length < 2) { + printUsage(); + return; + } + + long startTime = System.currentTimeMillis(); + + Language language = Language.EN; + String srcFolder = args[0]; + String dstFolder = args[1]; + String tempFolder = srcFolder+"/_temp/"; + int threads = 2; + boolean cleanTempFiles = false; + + try { + for (int n = 2; n < args.length; n++) { + if (args[n].startsWith("-lang")) { + language = Language.valueOf(args[++n]); + } else if (args[n].startsWith("-threads")) { + threads = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-cleanTempFiles")) { + cleanTempFiles = true; + } + } + } catch (RuntimeException re) { + printUsage(); + return; + } + + FatePacker packer; + + //Clean install folder + FileUtil.deleteFolder(new File(dstFolder)); + new File(dstFolder).mkdirs(); + + packer = new FatePacker(language.getLangCode(), srcFolder); + packer.process(new File(srcFolder), new File(tempFolder)); + + System.out.println("Installing "+language+"..."); + InstallerPacker.execute(String.format("create \"%s\" \"%s\"", tempFolder, dstFolder)); + + { + ResourceUsageAnalyzer rua = new ResourceUsageAnalyzer(srcFolder+"/_info", srcFolder); + rua.analyze(language, threads); + File depF = new File(srcFolder+"/_info/dependency_analysis"); + File[] files = depF.listFiles(); + if (files != null) { + for (File f : files) { + if (f.getName().endsWith(".xml")) { + try { + FileUtil.copyFile(f, new File(dstFolder+"/_installer/"+f.getName())); + } catch (IOException ioe) { + Log.w("Error trying to copy file: " + f, ioe); + } + } + } + } + } + System.gc(); + + File dst = new File(dstFolder); + try { + FileUtil.copyFile(new File("template/fate/instructions.txt"), dst); + FileUtil.copyFile(new File("FSNInstaller.jar"), dst); + copyFolderNoSVN(new File("lib"), new File(dstFolder+"/lib")); + } catch (IOException e) { + Log.e("Error trying to copy files", e); + } + + //Clean tempfolder + FileUtil.deleteFolder(new File(tempFolder)); + + if (cleanTempFiles) { + Log.v("Cleaning temp files..."); + + FileUtil.deleteFolder(new File(srcFolder+"/../data")); + FileUtil.deleteFolder(new File(srcFolder)); + } + + //Finished + Log.v(StringUtil.formatTime(System.currentTimeMillis()-startTime, TimeUnit.MILLISECONDS) + " Finished."); + } + + @Override + protected void modifyNameMapping(FileMapper mapper) { + mapper.put("info.txt", "info-" + language.toLowerCase() + ".txt"); + } + + protected static void copyFolderNoSVN(File src, File dst) throws IOException { + Map fileMap = new HashMap(); + FileUtil.collectFiles(fileMap, src, false); + for (Iterator> i = fileMap.entrySet().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.getValue().isDirectory()) { + continue; + } + + String key = entry.getKey().replace('\\', '/'); + if (key.contains("/.") || key.startsWith(".") || entry.getKey().equals("instructions")) { + //Remove unix-style hidden files (like .svn folders) + //Remove instructions.txt (install instructions) + i.remove(); + } + } + + new File(dst.getAbsolutePath()+'/'+src.getName()).mkdirs(); + FileUtil.copyFiles(fileMap, dst); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FatePatcher.java b/UI/src/nl/weeaboo/krkr/fate/FatePatcher.java new file mode 100644 index 0000000..a402f8e --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FatePatcher.java @@ -0,0 +1,238 @@ +package nl.weeaboo.krkr.fate; + +import java.util.HashMap; +import java.util.Map; + +import nl.weeaboo.krkr.fate.FateScriptConverter.Language; +import nl.weeaboo.vnds.Patcher; + +public class FatePatcher extends Patcher { + + private Language lang; + + public FatePatcher(Language lang) { + this.lang = lang; + } + + //Functions + public void fillAppendMap(Map appendMap) { + String temp; + + //Prologue + { + appendMap.put("prologue00.ks", "jump prologue01.scr"); + appendMap.put("prologue01.ks", "jump prologue02.scr"); + appendMap.put("prologue02.ks", "text \njump main.scr"); + } + + //Fate + { + //Subroutines aren't supported, so I have to change around the append-data a bit + temp = appendMap.remove("fate15-17.ks"); + appendMap.put("fate-ending.ks", (temp != null ? temp : "") + "\njump fate15-17.scr post"); + } + + //UBW + { + //Subroutines aren't supported, so I have to change around the append-data a bit + temp = appendMap.remove("ubw14-15.ks"); + appendMap.put("ubw-ending.ks", (temp != null ? temp : "") + "\njump ubw14-15.scr post"); + + temp = appendMap.remove("ubw14-16.ks"); + appendMap.put("ubw-ending2.ks", (temp != null ? temp : "") + "\njump ubw14-16.scr post"); + } + + //HF + { + //Subroutines aren't supported, so I have to change around the append-data a bit + temp = appendMap.remove("hf16-09.ks"); + appendMap.put("hf-ending2.ks", (temp != null ? temp : "") + "\njump hf16-09.scr post"); + + temp = appendMap.remove("hf16-13.ks"); + appendMap.put("hf-ending.ks", (temp != null ? temp : "") + "\njump hf16-13.scr post"); + } + } + + public void patchPre(Map> patch) { + //Shared + + //Language Dependent + if (lang == Language.EN) { + patchPreEN(patch); + } else if (lang == Language.JA) { + patchPreJA(patch); + } else if (lang == Language.CH) { + patchPreCH(patch); + } + } + public void patchPreEN(Map> patch) { + //Warning, pre-patching may be broken + } + public void patchPreJA(Map> patch) { + //Warning, pre-patching may be broken + } + public void patchPreCH(Map> patch) { + //Warning, pre-patching may be broken + } + + public void patchPost(Map> patch) { + Map map = null; + + { + //Prologue + map = patchPost(patch, "prologue-00.ks"); + map.put(5, "#type moon logo removed"); + + //Fate + map = patchPost(patch, "fate01-00.ks"); + map.put(6, RM_TEXT); + map.put(8, "bgload special/fate/bone1.jpg\ntext I am the bone of my sword."); + map.put(9, "bgload special/fate/bone2.jpg\ntext Steel is my body, and fire is my blood."); + map.put(10, "bgload special/fate/bone3.jpg\ntext I have created over a thousand blades."); + map.put(20, RM_TEXT); + map.put(21, "bgload special/fate/bone4.jpg\ntext Unknown to Death."); + map.put(22, "bgload special/fate/bone5.jpg\ntext Nor known to Life."); + map.put(23, "bgload special/fate/bone6.jpg\ntext Have withstood pain to create many weapons."); + map.put(24, "bgload special/fate/bone7.jpg\ntext Yet, those hands will never hold anything."); + map.put(25, "bgload special/fate/bone8.jpg\ntext So as I pray, unlimited blade works."); + map.put(40, RM_TEXT); + map.put(43, RM_TEXT); + map.put(44, RM_TEXT); + + //UBW + map = patchPost(patch, "ubw14-15.ks"); + map.put(5, "jump ubw-ending.scr\nlabel post"); + + map = patchPost(patch, "ubw14-16.ks"); + map.put(5, "jump ubw-ending2.scr\nlabel post"); + + //HF + + } + + //Language Dependent + if (lang == Language.EN) { + patchPostEN(patch); + } else if (lang == Language.JA) { + patchPostJA(patch); + } else if (lang == Language.CH) { + patchPostCH(patch); + } + } + public void patchPostEN(Map> patch) { + Map map = null; + + //Prologue + + //Fate + map = patchPost(patch, "fate04-05.ks"); + map.put(757, "#status screen related"); + map = patchPost(patch, "fate04-18.ks"); + map.put(111, "#status screen related"); + map = patchPost(patch, "fate05-11.ks"); + map.put(96, "#status screen related"); + + map = patchPost(patch, "fate15-17.ks"); + map.put(319, "jump fate-ending.scr\nlabel post"); + + //UBW + map = patchPost(patch, "ubw04-10.ks"); + map.put(507, "#status screen related"); + + //HF (untranslated) + map = patchPost(patch, "hf04-10.ks"); + map.put(886, "#status screen related"); + + map = patchPost(patch, "hf16-09.ks"); + map.put(307, "jump hf-ending2.scr\nlabel post"); + + map = patchPost(patch, "hf16-12.ks"); + map.put(305, "jump hf-ending2.scr\nlabel post"); + + map = patchPost(patch, "hf16-13.ks"); + map.put(395, "jump hf-ending.scr\nlabel post"); + } + public void patchPostJA(Map> patch) { + Map map = null; + + //Prologue + + //Fate + map = patchPost(patch, "fate04-05.ks"); + map.put(925, "#status screen related"); + map = patchPost(patch, "fate04-18.ks"); + map.put(131, "#status screen related"); + map = patchPost(patch, "fate05-11.ks"); + map.put(130, "#status screen related"); + + map = patchPost(patch, "fate15-17.ks"); + map.put(350, "jump fate-ending.scr\nlabel post"); + + //UBW + map = patchPost(patch, "ubw04-10.ks"); + map.put(628, "#status screen related"); + + //HF + map = patchPost(patch, "hf04-10.ks"); + map.put(886, "#status screen related"); + + map = patchPost(patch, "hf16-09.ks"); + map.put(307, "jump hf-ending2.scr\nlabel post"); + + map = patchPost(patch, "hf16-12.ks"); + map.put(305, "jump hf-ending2.scr\nlabel post"); + + map = patchPost(patch, "hf16-13.ks"); + map.put(395, "jump hf-ending.scr\nlabel post"); + } + public void patchPostCH(Map> patch) { + Map map = null; + + //Prologue + + //Fate + map = patchPost(patch, "fate04-05.ks"); + map.put(987, "#status screen related"); + map = patchPost(patch, "fate04-18.ks"); + map.put(138, "#status screen related"); + map = patchPost(patch, "fate05-11.ks"); + map.put(136, "#status screen related"); + + map = patchPost(patch, "fate15-17.ks"); + map.put(349, "jump fate-ending.scr\nlabel post"); + + //UBW + map = patchPost(patch, "ubw04-10.ks"); + map.put(683, "#status screen related"); + + //HF + map = patchPost(patch, "hf04-10.ks"); + map.put(1096, "#status screen related"); + + map = patchPost(patch, "hf16-09.ks"); + map.put(321, "jump hf-ending2.scr\nlabel post"); + + map = patchPost(patch, "hf16-12.ks"); + map.put(320, "jump hf-ending2.scr\nlabel post"); + + map = patchPost(patch, "hf16-13.ks"); + map.put(454, "jump hf-ending.scr\nlabel post"); + } + + protected Map patchPost(Map> patchPost, String filename) { + Map map = new HashMap(); + patchPost.put(filename, map); + return map; + } + + protected Map patchPre(Map> patchPre, String filename) { + Map map = new HashMap(); + patchPre.put(filename, map); + return map; + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateResourceConverter.java b/UI/src/nl/weeaboo/krkr/fate/FateResourceConverter.java new file mode 100644 index 0000000..978b111 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateResourceConverter.java @@ -0,0 +1,243 @@ +package nl.weeaboo.krkr.fate; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileCollectFilter; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.system.ProcessUtil; +import nl.weeaboo.vnds.AbstractResourceConverter; +import nl.weeaboo.vnds.BatchProcess; +import nl.weeaboo.vnds.FileOp; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.tools.ImageConverter; +import nl.weeaboo.vnds.tools.ImageConverter.ScalingType; +import nl.weeaboo.vnds.tools.SoundConverter; + +public class FateResourceConverter extends AbstractResourceConverter { + + public FateResourceConverter() { + } + + //Functions + public static void main(String args[]) { + System.setProperty("line.separator", "\n"); + + FateResourceConverter e = new FateResourceConverter(); + try { + e.parseCommandLine(args, 2); + } catch (IOException ioe) { + printUsage(e.getClass()); + return; + } + + try { + e.extract(args[0], args[1]); + } catch (IOException ioe) { + Log.e("Fatal error during resource conversion", ioe); + } + } + public void extract(String src, String dst) throws IOException { + File dstF = new File(dst); + File originalF = new File(dstF, "_original"); + File generatedF = new File(dstF, "_generated"); + + //Clean up _original folder + FileUtil.deleteFolder(originalF); + originalF.mkdirs(); + + //Extract game data + FateExtractor.main(new String[] {src, originalF.getAbsolutePath()}); + + //Clean up _generated folder + initOutputFolder(generatedF); + + //Convert + + convertBackground(dstF); + convertForeground(dstF); + convertSound(dstF); + convertMusic(dstF); + + //Template + File templateF = new File("template/fate"); + copyTemplate(templateF, generatedF); + + //Done + Log.v("Done"); + } + + public void convertBackground(final File root) { + Log.v("Converting backgrounds..."); + + final ImageConverter ic = createBackgroundConverter(); + + Map files = new HashMap(); + FileUtil.collectFiles(files, new File(root, "/_original/bgimage"), false, false, new FileCollectFilter() { + public boolean accept(String relpath, File file) { + if (file.isDirectory()) return true; + return relpath.endsWith("tlg") || relpath.endsWith("bmp"); + } + }); + + BatchProcess bp = createBatch(); + try { + bp.run(files, new FileOp() { + @Override + public void execute(String relpath, File file) throws IOException { + if (relpath.endsWith(".tlg")) { + file = convertTLG(file); + } + ic.convertFile(file, new File(root, "_generated/background")); + } + }); + } catch (InterruptedException ie) { + Log.w("Batch Process Interrupted"); + } + } + + public void convertForeground(final File root) { + Log.v("Converting sprites..."); + + final ImageConverter ic = createForegroundConverter(); + ic.setScalingType(ScalingType.SPRITE); + + Map files = new HashMap(); + FileUtil.collectFiles(files, new File(root, "/_original/fgimage"), false, false, new FileCollectFilter() { + public boolean accept(String relpath, File file) { + if (file.isDirectory()) return true; + return relpath.endsWith("tlg") || relpath.endsWith("bmp"); + } + }); + + BatchProcess bp = createBatch(); + try { + bp.run(files, new FileOp() { + @Override + public void execute(String relpath, File file) throws IOException { + if (relpath.endsWith(".tlg")) { + file = convertTLG(file); + } + ic.convertFile(file, new File(root, "_generated/foreground")); + } + }); + } catch (InterruptedException ie) { + Log.w("Batch Process Interrupted"); + } + } + + protected static File convertTLG(File tlgF) throws IOException { + String hash = "__" + Long.toHexString(Thread.currentThread().getId()) + "__"; + + File tempTLG = new File(tlgF.getParentFile(), hash + ".tlg"); + File tempBMP = new File(tlgF.getParentFile(), hash + ".bmp"); + + File bmpF = new File(StringUtil.replaceExt(tlgF.getAbsolutePath(), "bmp")); + bmpF.delete(); + + tlgF.renameTo(tempTLG); + + try { + Process p = ProcessUtil.execInDir( + String.format("tlg2bmp \"%s\" \"%s\"", + tempTLG.getAbsolutePath(), tempBMP.getAbsolutePath()), + "tools/"); + ProcessUtil.waitFor(p); + } finally { + tempTLG.delete(); + tempBMP.renameTo(bmpF); + } + + return bmpF; + } + + public void convertSound(File root) { + final File targetFolder = new File(root, "_generated/sound"); + + try { + Log.v("Converting SFX..."); + final SoundConverter sc = createSFXEncoder(); + + Map files = new HashMap(); + FileUtil.collectFiles(files, new File(root, "/_original/sound"), false, false, new FileCollectFilter() { + public boolean accept(String relpath, File file) { + if (file.isDirectory()) return true; + return relpath.endsWith("wav") || relpath.endsWith("ogg"); + } + }); + + BatchProcess bp = createBatch(); + bp.run(files, new FileOp() { + @Override + public void execute(String relpath, File file) throws IOException { + sc.convertFile(file, targetFolder); + } + }); + } catch (InterruptedException ie) { + Log.w("Batch Process Interrupted"); + } + + if (convertVoice) { + Log.v("Converting Voice..."); + final SoundConverter sc = createVoiceEncoder(); + + Map files = new HashMap(); + FileUtil.collectFiles(files, new File(root, "/_original/patch6"), false, false, new FileCollectFilter() { + public boolean accept(String relpath, File file) { + if (file.isDirectory()) return true; + return relpath.endsWith("wav") || relpath.endsWith("ogg"); + } + }); + + BatchProcess bp = createBatch(); + bp.setTaskSize(250); + try { + bp.run(files, new FileOp() { + @Override + public void execute(String relpath, File file) throws IOException { + sc.convertFile(file, targetFolder); + } + }); + } catch (InterruptedException ie) { + Log.w("Batch Process Interrupted"); + } + } + } + + public void convertMusic(final File root) { + final File targetFolder = new File(root, "_generated/sound"); + + Log.v("Converting music..."); + final SoundConverter sc = createMusicEncoder(); + + //Convert music + Map files = new HashMap(); + FileUtil.collectFiles(files, new File(root, "/_original/bgm"), false, false, new FileCollectFilter() { + public boolean accept(String relpath, File file) { + if (file.isDirectory()) return true; + return relpath.endsWith("wav") || relpath.endsWith("ogg"); + } + }); + + BatchProcess bp = createBatch(); + bp.setTaskSize(5); + try { + bp.run(files, new FileOp() { + @Override + public void execute(String relpath, File file) throws IOException { + sc.convertFile(file, targetFolder); + } + }); + } catch (InterruptedException ie) { + Log.w("Batch Process Interrupted"); + } + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateScriptConverter.java b/UI/src/nl/weeaboo/krkr/fate/FateScriptConverter.java new file mode 100644 index 0000000..65eef9e --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateScriptConverter.java @@ -0,0 +1,185 @@ +package nl.weeaboo.krkr.fate; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.krkr.KiriKiriConverter; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.Patcher; + +public class FateScriptConverter extends KiriKiriConverter { + + //----------------------------------------------------- + //Fixed after 1.0 release: + //----------------------------------------------------- + // * Removed a few lines where KrKr commands were printed in the .scr files + // * Blank lines removed from EN edition + // * Updated English translation to version 3.2 + // - 1.1.4 -------------------------------------------- + // * Reduced XP3 extractor memory usage + // * UTF-8 support for the conversion GUI log + // * Other memory reductions + + public enum Language { + EN("en", "UTF-16"), JA("ja", "SJIS"), CH("ch", "UTF-16LE"); + + private String encoding; + private String langCode; + + private Language(String langCode, String encoding) { + this.langCode = langCode; + this.encoding = encoding; + } + + public String getEncoding() { return encoding; } + public String getLangCode() { return langCode; } + }; + + private boolean insertVoiceData; + private Language lang; + + private int allowedRoutes = 4; //1=Prologue, 2=P+Fate, 3=P+F+UBW, 4=P+F+UBW+HF + private RouteParser routeParser; + private boolean soundPlaying; + + public FateScriptConverter(File srcF, File dstF, String language, boolean insertVoice) { + super(srcF, new File(srcF, "data"), dstF); + + lang = Language.valueOf(language); + insertVoiceData = insertVoice; + + if (insertVoiceData) { + if (lang == Language.CH) { + System.err.println("Voice support for the Chinese language is not available"); + } else { + //Inserts the @say commands into the Japanese version + // -- don't run this on the same file more than once + + /*new InsertVoice().patch(getRootFolder() + "/scenario-" + lang.getLangCode(), + lang.getEncoding(), getRootFolder() + "/scenario-" + Language.EN.getLangCode(), + Language.EN.getEncoding()); + */ + throw new RuntimeException("TODO: Fix voice insertion"); + } + } + + setSourceFileEncoding(lang.getEncoding()); + + macroParser.addMacroHandler(new FateMacroHandler(this, macroParser, fileExts)); + macroParser.addTextMacroHandler(new FateTextMacroHandler(this, macroParser)); + } + + //Functions + public static void main(String args[]) { + File srcF = new File(args[0]); + File dstF = new File(args[1]); + String lang = args[2]; + + Log.v("Converting scripts..."); + FateScriptConverter converter = new FateScriptConverter(srcF, dstF, lang, false); + converter.convert(); + } + + protected Patcher createPatcher() { + return new FatePatcher(lang); + } + + protected String createOutputPath(String filename) { + return super.createOutputPath(RouteParser.scenarioFileRename(this, filename)); + } + + public void convert() { + routeParser = new RouteParser(this); + + super.convert(); + } + + protected void scriptConvert(int pass, String relpath, File file) { + if (pass == 1 && file.getName().endsWith("fcf")) { + if (!relpath.contains("/")) { + //If any subfolders exist, it's because the files were extracted on top of + //an aborted earlier extraction. + routeParser.parse(file, appendMap); + } + } else if (pass == 2 && file.getName().endsWith("ks")) { + try { + if (!routeParser.isIncludedInThisRun(file.getName())) { + return; + } + processKSFile(file); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + protected void processKSFile(File file) throws IOException { + soundPlaying = false; + + super.processKSFile(file, RouteParser.scenarioFileRename(this, file.getName())+".ks"); + + if (lang != Language.EN) { + //Postprocess: remove blank lines and merge lines that belong to a single sentence + + StringBuilder sb = new StringBuilder(); + StringBuilder string = new StringBuilder(); + + String path = createOutputPath(file.getName()); + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8")); + String line; + while ((line = in.readLine()) != null) { + if (line.startsWith("text ")) { + String textPart = line.substring(5).trim(); + + if (textPart.length() > 0 && textPart.charAt(0) == 0x3000) { + if (string.length() > 0) { + sb.append("text "); + sb.append(string); + sb.append("\n"); + string.delete(0, string.length()); + } + } + string.append(textPart); + } else { + if (string.length() > 0) { + sb.append("text "); + sb.append(string); + sb.append("\n"); + string.delete(0, string.length()); + } + + sb.append(line); + sb.append("\n"); + } + } + in.close(); + + FileUtil.write(new File(path), sb.toString()); + } + } + + protected String parseText(String filename, int lineNumber, String line) { + String soundAppend = ""; + if (lang == Language.EN && soundPlaying) { + //soundAppend = "\nsound ~"; + //soundPlaying = false; + } + + String result = macroParser.flush(filename, lineNumber, line); + return result + (result.length() > 0 ? "\n" : "") + "text " + + super.parseText(filename, lineNumber, line) + soundAppend; + } + + //Getters + public int getAllowedRoutes() { return allowedRoutes; } + public boolean isSoundPlaying() { return soundPlaying; } + public Language getLanguage() { return lang; } + public int getNumberOfPasses() { return 2; } + + //Setters + public void setSoundPlaying(boolean sp) { this.soundPlaying = sp; } +} diff --git a/UI/src/nl/weeaboo/krkr/fate/FateTextMacroHandler.java b/UI/src/nl/weeaboo/krkr/fate/FateTextMacroHandler.java new file mode 100644 index 0000000..5c913da --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/FateTextMacroHandler.java @@ -0,0 +1,87 @@ +package nl.weeaboo.krkr.fate; + +import java.io.IOException; +import java.util.Map; + +import nl.weeaboo.krkr.KiriKiriConverter; +import nl.weeaboo.krkr.MacroHandler; +import nl.weeaboo.krkr.MacroParser; + +public class FateTextMacroHandler extends MacroHandler { + + //TODO: + //p <- Wait for user click, without inserting a newline + //ruby <- Read-Hint? Red Text? + //vr <- ??? + + public FateTextMacroHandler(KiriKiriConverter krkr, MacroParser mp) { + super(krkr, mp); + + String ignore[] = new String[] { + "aero", "atlas", "keraino", "troya", "margos", "heart", + "l", "r", + }; + + for (String s : ignore) { + ignore(s); + } + } + + //Functions + public String process(String macro, Map params) throws IOException { + if (macro.equals("wrap")) { + return ""; + } else if (macro.equals("szlig")) { + return String.valueOf(Character.toChars(0x00DF)); + } else if (macro.equals("XAuml")) { + return String.valueOf(Character.toChars(0x00C4)); + } else if (macro.equals("XOuml")) { + return String.valueOf(Character.toChars(0x00D6)); + } else if (macro.equals("XUuml")) { + return String.valueOf(Character.toChars(0x00DC)); + } else if (macro.equals("auml")) { + return String.valueOf(Character.toChars(0x00E4)); + } else if (macro.equals("ouml")) { + return String.valueOf(Character.toChars(0x00F6)); + } else if (macro.equals("uuml")) { + return String.valueOf(Character.toChars(0x00FC)); + } else if (macro.equals("wacky")) { + return parse_wacky(params); + } else if (macro.equals("block")) { + return parse_block(params); + } else if (macro.startsWith("line")) { + return parse_line(macro, params); + } else if (macro.startsWith("ruby")) { + return parse_ruby(macro, params); + } + + return super.process(macro, params); + } + + private String parse_block(Map params) { + int len = Integer.parseInt(params.get("len")); + return KiriKiriConverter.repeatString("-", len); + } + + private String parse_wacky(Map params) { + int len = Integer.parseInt(params.get("len")); + len = Math.min(6, len); + + return "R" + KiriKiriConverter.repeatString("O", len) + + KiriKiriConverter.repeatString("A", Math.max(1, len/2)) + "R"; + } + + private String parse_line(String macro, Map params) { + int num = Integer.parseInt(macro.substring(4)); + return KiriKiriConverter.repeatString("-", num * 2); + } + + private String parse_ruby(String macro, Map params) { + return ""; //params.get("text"); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/InsertVoice.java b/UI/src/nl/weeaboo/krkr/fate/InsertVoice.java new file mode 100644 index 0000000..0ea2f6e --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/InsertVoice.java @@ -0,0 +1,136 @@ +package nl.weeaboo.krkr.fate; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Hashtable; +import java.util.Map; +import java.util.Map.Entry; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; + +public class InsertVoice { + + //Functions + public void patch(String folderJ, String encodingJ, String folderE, String encodingE) { + Map filesJ = new Hashtable(); + FileUtil.collectFiles(filesJ, new File(folderJ), false); + + Map filesE = new Hashtable(); + FileUtil.collectFiles(filesE, new File(folderE), false); + + for (Entry entry : filesE.entrySet()) { + File jFile = filesJ.get(entry.getKey()); + if (jFile != null) { + try { + patchFile(jFile, encodingJ, entry.getValue(), encodingE); + } catch (IOException e) { + Log.e("Error patching voices", e); + } + } + } + } + + protected void patchFile(File fileJ, String encodingJ, File fileE, String encodingE) throws IOException { + BufferedReader japIn = new BufferedReader(new InputStreamReader(new FileInputStream(fileJ), encodingJ)); + BufferedReader engIn = new BufferedReader(new InputStreamReader(new FileInputStream(fileE), encodingE)); + + + StringBuilder sb = new StringBuilder(); + + String lineEng; + String lineJap; + + int engPage = -1; + int japPage = -1; + + while ((lineEng = engIn.readLine()) != null) { + if (lineEng.startsWith("@say")) { + //sb.append(l + ":"); + sb.append(lineEng); + sb.append('\n'); + //sb.append("------------------------------------------------------\n"); + } else if (engPage >= japPage) { + do { + int t = 0; + boolean append = false; + while ((lineJap = japIn.readLine()) != null) { + japPage = Math.max(japPage, pageFromLine(lineJap)); + + char firstChar = '\0'; + char lastChar = '\0'; + String temp = stripTags(lineJap); + if (temp.length() > 0) { + firstChar = temp.charAt(0); + lastChar = temp.charAt(temp.length()-1); + } + + if (t == 0) { + //sb.append(l + ":"); + sb.append(lineJap); + sb.append('\n'); + } + + if (firstChar == 0x40 || firstChar == 0x2A ) { + break; + } else if (t == 0 || (t > 0 && ((firstChar != 0x3000 && firstChar != 0x300C) || append))) { + if (t > 0) { + //sb.append(l + ":"); + sb.append(lineJap); + sb.append('\n'); + } + + append = (lastChar == 0x3001); + japIn.mark(1024); + t++; + } else { + break; + } + } + if (t > 0) { + japIn.reset(); + } + //sb.append("------------------------------------------------------\n"); + //System.out.println(engPage + " " + japPage); + } while (engPage > japPage); + } + engPage = Math.max(engPage, pageFromLine(lineEng)); + } + + japIn.close(); + engIn.close(); + + System.out.println("Inserting Voice Data: " + fileJ.getName()); + //fileJ = new File("e:/temp.txt"); + FileOutputStream fout = new FileOutputStream(fileJ); + fout.write(sb.toString().getBytes(encodingJ)); + fout.flush(); + fout.close(); + } + + protected String stripTags(String s) { + return s.replaceAll("\\[[^\\]]\\]", ""); + } + + protected int pageFromLine(String line) { + if (line.startsWith("*page")) { + int index = 5; + while (index < line.length() && Character.isDigit(line.charAt(index))) { + index++; + } + try { + return Integer.parseInt(line.substring(5, index)); + } catch (Exception e) { + } + } + return -1; + } + + //Getters + + //Setters +} diff --git a/UI/src/nl/weeaboo/krkr/fate/OutputField.java b/UI/src/nl/weeaboo/krkr/fate/OutputField.java new file mode 100644 index 0000000..e31eb9b --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/OutputField.java @@ -0,0 +1,72 @@ +package nl.weeaboo.krkr.fate; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.string.HtmlUtil; + +@SuppressWarnings("serial") +public class OutputField extends JPanel { + + private JLabel textArea; + private JScrollPane scrollPane; + + public OutputField() { + textArea = createTextArea(); + + scrollPane = new JScrollPane(textArea); + scrollPane.getViewport().setBackground(new Color(241, 241, 241)); + + setPreferredSize(new Dimension(400, 150)); + setLayout(new BorderLayout(2, 2)); + add(scrollPane, BorderLayout.CENTER); + } + + protected JLabel createTextArea() { + JLabel label = new JLabel(); + label.setBorder(new EmptyBorder(5, 5, 5, 5)); + label.setVerticalTextPosition(JLabel.TOP); + label.setVerticalAlignment(JLabel.TOP); + return label; + } + + protected JProgressBar createGlobalProgressBar() { + JProgressBar bar = new JProgressBar(0, 1000); + return bar; + } + protected JProgressBar createLocalProgressBar() { + JProgressBar bar = new JProgressBar(0, 100); + return bar; + } + + //Functions + public void setText(String t) { + textArea.setText("" + HtmlUtil.escapeHtml(t) + ""); + textArea.validate(); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + JScrollBar bar = scrollPane.getVerticalScrollBar(); + if (bar != null) { + bar.setValue(bar.getMaximum()); + } + } + }); + + repaint(); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/RealtaNuaSoundExtractor.java b/UI/src/nl/weeaboo/krkr/fate/RealtaNuaSoundExtractor.java new file mode 100644 index 0000000..c68e937 --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/RealtaNuaSoundExtractor.java @@ -0,0 +1,147 @@ +package nl.weeaboo.krkr.fate; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.system.ProcessUtil; +import nl.weeaboo.vnds.Log; + +public class RealtaNuaSoundExtractor { + + public RealtaNuaSoundExtractor() { + } + + //Functions + public static void main(String args[]) { + RealtaNuaSoundExtractor se = new RealtaNuaSoundExtractor(); + try { + se.extract(args[0], args[1]); + } catch (FileNotFoundException fnfe) { + System.err.println(fnfe); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static final int read_s32(InputStream in) throws IOException { + return (int)readLE(in, 4); + } + public static final long read_s64(InputStream in) throws IOException { + return readLE(in, 8); + } + public static final long readLE(InputStream in, int bytes) throws IOException { + int result = 0; + for (int n = 0; n < bytes; n++) { + result += (in.read() << (8 * n)); + } + return result; + } + + public void extract(String discRoot, String outFolder) throws IOException { + File outFolderFile = new File(outFolder); + outFolderFile.mkdirs(); + + File newExe = new File(outFolder+"/ahx2wav.exe"); + FileUtil.copyFile(new File("tools/ahx2wav_v014/ahx2wav.exe"), newExe); + FileInputStream fin = new FileInputStream(discRoot+"/data0.bin"); + FileChannel fc = fin.getChannel(); + + Log.v("Extracting AHX sound files..."); + + try { + byte sig[] = new byte[] {(byte)'A', (byte)'F', (byte)'S', (byte)'\0'}; + byte arcsig[] = new byte[4]; + fin.read(arcsig); + for (int n = 0; n < 4; n++) { + if (arcsig[n] != sig[n]) throw new IOException("FileFormat Error"); + } + + //0x808 -- file offset/size table offset + //0x466C3000 -- filename table offset + //0x00159660 -- filename table length + //48 -- filename table entry length + //29747 -- number of files + + int filesL = 29474; + FileEntry files[] = new FileEntry[filesL]; + + fc.position(0x808); + for (int n = 0; n < filesL; n++) { + files[n] = new FileEntry(); + files[n].offset = 0x800 + read_s32(fin); + files[n].length = read_s32(fin); + } + + byte nameBuffer[] = new byte[32]; + for (int n = 0; n < filesL; n++) { + fc.position(0x466C3000 + 0x30 * n); + fin.read(nameBuffer); + int l = 0; + while (l < nameBuffer.length && nameBuffer[l] != '\0') l++; + files[n].filename = new String(nameBuffer, 0, l); + } + + for (int n = 0; n < filesL; n++) { + if (n % 256 == 0) { + Log.v(String.format("(%d/%d) %s...", n+1, filesL, files[n].filename)); + } + + File outFile = new File(outFolder+'/'+files[n].filename); + FileOutputStream fout = new FileOutputStream(outFile); + int r = 0; + while (r < files[n].length) { + r += fc.transferTo(files[n].offset+r, files[n].length-r, fout.getChannel()); + } + fout.flush(); + fout.close(); + + //Convert to wav + Process p = ProcessUtil.execInDir(String.format( + "ahx2wav %s", + files[n].filename), + outFolder); + ProcessUtil.waitFor(p); + if (p.exitValue() != 0) { + throw new IOException("Error converting file: " + outFile.getAbsolutePath() + "\nAborting sound extraction."); + } + ProcessUtil.kill(p); + + //Delete original + outFile.delete(); + } + + //Rename a.b.wav to a.wav + File converted[] = outFolderFile.listFiles(); + for (File f : converted) { + String filename = f.getName(); + if (filename.endsWith(".wav")) { + filename = filename.substring(0, filename.indexOf('.')+1) + "wav"; + f.renameTo(new File(outFolder+'/'+filename)); + } + } + + Log.v("Done..."); + } finally { + fc.close(); + fin.close(); + newExe.delete(); + } + } + + //Getters + + //Setters + + //Inner Classes + private static class FileEntry { + public String filename; + public int offset; + public int length; + } +} diff --git a/UI/src/nl/weeaboo/krkr/fate/ResourceUsageAnalyzer.java b/UI/src/nl/weeaboo/krkr/fate/ResourceUsageAnalyzer.java new file mode 100644 index 0000000..f8df5ef --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/ResourceUsageAnalyzer.java @@ -0,0 +1,483 @@ +package nl.weeaboo.krkr.fate; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.krkr.fate.FateScriptConverter.Language; +import nl.weeaboo.string.StringUtil2; +import nl.weeaboo.vnds.FileMapper; +import nl.weeaboo.vnds.HashUtil; +import nl.weeaboo.vnds.Log; + +public class ResourceUsageAnalyzer { + + private static final int ROUTE_PRO = 1; + private static final int ROUTE_FATE = 2; + private static final int ROUTE_UBW = 4; + private static final int ROUTE_HF = 8; + + private Map names; + private String infoFolder; + private String rootFolder; + + public ResourceUsageAnalyzer(String infoFolder, String rootFolder) { + this.infoFolder = infoFolder; + this.rootFolder = rootFolder; + + names = new HashMap(); + names.put(0, "core"); + names.put(ROUTE_PRO, "prologue"); + names.put(ROUTE_FATE, "route01-fate"); + names.put(ROUTE_UBW, "route02-ubw"); + names.put(ROUTE_HF, "route03-hf"); + } + + //Functions + protected Map> a() { + Map files = new Hashtable(); + FileUtil.collectFiles(files, new File(rootFolder+"/script"), false); + + System.out.println("Collecting Resources..."); + + Map resources = new Hashtable(); + for (Entry entry : files.entrySet()) { + FileInfo info = new FileInfo(entry.getKey()); + resources.put(info.getFilename(), info); + + try { + analyzeResources(rootFolder+"/script", info); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + System.out.println("Writing Resource Lists to Disk..."); + + File outputFolder = new File(infoFolder, "dependency_analysis"); + FileUtil.deleteFolder(outputFolder); + outputFolder.mkdirs(); + for (FileInfo info : resources.values()) { + try { + info.save(outputFolder+StringUtil.stripExtension(info.getFilename())+".txt"); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + System.out.println("Graph Analysis..."); + + Map nodes = new HashMap(); + for (FileInfo info : resources.values()) { + nodes.put(info.getFilename(), new GraphNode(info.getFilename())); + } + for (FileInfo info : resources.values()) { + GraphNode in = nodes.get(info.getFilename()); + for (String s : info.jumpsTo) { + GraphNode out = nodes.get(s); + if (in != null && out != null) { + in.outgoing.add(out); + out.incoming.add(in); + } + } + } + + setRoute(resources, nodes, "prologue00.scr", ROUTE_PRO); + setRoute(resources, nodes, "fate05-00.scr", ROUTE_FATE); + setRoute(resources, nodes, "ubw03-09.scr", ROUTE_UBW); + setRoute(resources, nodes, "ubw04-03.scr", ROUTE_UBW); + setRoute(resources, nodes, "hf04-11.scr", ROUTE_HF); + + SortedSet sorted = new TreeSet(StringUtil2.getStringComparator()); + sorted.addAll(resources.keySet()); + + for (int n = 0; n < 3; n++) { + StringBuilder sb = new StringBuilder(); + for (String name : sorted) { + GraphNode node = nodes.get(name); + StringBuilder sb2 = new StringBuilder(); + + String prefix = node.getName().substring(0, Math.min(node.getName().length(), 2)); + int links = 0; + + sb2.append(node.getName()+" "+resources.get(name).getRoute()); + sb2.append("\n"); + + if (n < 2) { + sb2.append("\tin : "); + for (GraphNode in : node.incoming) { + if (n==0 || !in.getName().startsWith(prefix)) { + sb2.append(in.getName()+" "); + links++; + } + } + sb2.append("\n"); + + sb2.append("\tout: "); + for (GraphNode in : node.outgoing) { + if (n==0 || !in.getName().startsWith(prefix)) { + sb2.append(in.getName()+" "); + links++; + } + } + sb2.append("\n"); + } + if (n == 0) { + sb.append(sb2); + sb.append("\n\n"); + } else if (n == 2 || links > 0) { + sb.append(sb2); + } + } + try { + FileUtil.write(new File(outputFolder+"_graph"+n+".txt"), sb.toString()); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + System.out.println("Creating XML files..."); + + Map> res = new HashMap>(); + for (FileInfo info : resources.values()) { + Set set = res.get(info.getRoute()); + if (set == null) { + res.put(info.getRoute(), set = new HashSet()); + } + + for (String s : info.background) set.add("background/"+s); + for (String s : info.foreground) set.add("foreground/"+s); + for (String s : info.sound) set.add("sound/"+s); + for (String s : info.music) set.add("music/"+s); + } + + return res; + } + + public void analyze(Language lang, int threads) { + long tstart = System.currentTimeMillis(); + + Map> res = a(); + + //Generate set of all global resources, including the font, info.txt, etc. + Set globalResources = res.get(0); + globalResources.add("info.txt"); + globalResources.add("default.ttf"); + globalResources.add("thumbnail.png"); + globalResources.add("icon.png"); + globalResources.add("img.ini"); + globalResources.add("save/global.sav"); + addFolder(globalResources, "script", "script"); + addFolder(globalResources, "foreground/special", "foreground/special"); + addFolder(globalResources, "background/special", "background/special"); + addFolder(globalResources, "sound/special", "sound/special");; + + FileMapper mapper = new FileMapper(); + mapper.put("info.txt", "info-"+lang.getLangCode()+".txt"); + try { + mapper.load(infoFolder+"/filenames.txt"); + } catch (IOException ioe) { + Log.e("Exception loading filename mapping", ioe); + } + + for (Entry> entry : res.entrySet()) { + try { + hashFileSet(names.get(entry.getKey()), entry.getValue(), mapper); + } catch (IOException e) { + Log.e("Error hashing file set", e); + } + } + + System.out.printf("Took %.2fs\nFinished\n", (System.currentTimeMillis()-tstart)/1000.0); + } + + protected void hashFileSet(String name, Set set, FileMapper mapper) throws IOException { + System.out.println("Hashing files ("+set.size()+")..."); + + String outputFolder = infoFolder+"/dependency_analysis/"; + String outputFilename = outputFolder+name+".xml"; + File outputF = new File(outputFilename); + + PrintWriter pout = new PrintWriter(new OutputStreamWriter( + new BufferedOutputStream(new FileOutputStream(outputF)), "UTF-8")); + try { + pout.println("\n"); + + ExecutorService executor = Executors.newFixedThreadPool(2); + List> results = new ArrayList>(); + + int t = 0; + Iterator i = set.iterator(); + while (t < set.size()) { + String strings[] = new String[Math.min(set.size()-t, 512)]; + for (int n = 0; n < strings.length; n++) { + strings[n] = i.next(); + } + + Callable task = new FileHashingTask(strings, rootFolder, mapper); + results.add(executor.submit(task)); + t += strings.length; + } + set.clear(); + + for (Future f : results) { + try { + pout.print(f.get()); + } catch (ExecutionException e) { + Log.w("Exception getting hash results", e); + } + } + + executor.shutdown(); + + pout.println("\n"); + } catch (InterruptedException e) { + Log.w("Wait for hashing thread pool interrupted", e); + } finally { + pout.close(); + } + } + + private void addFolder(Set globalResources, String sourceFolder, String targetFolder) { + File folder = new File(rootFolder+"/"+sourceFolder); + if (folder.exists()) { + for (File f : folder.listFiles()) { + if (f.isDirectory()) { + addFolder(globalResources, sourceFolder+'/'+f.getName(), targetFolder+'/'+f.getName()); + } else { + globalResources.add(targetFolder+"/"+f.getName()); + } + } + } else { + Log.w("Folder not found: " + folder.getAbsolutePath()); + } + } + + protected void analyzeResources(String folder, FileInfo info) throws IOException { + String text = FileUtil.read(new File(folder, info.getFilename())); + for (String line : text.split("\\\n")) { + String parts[] = line.trim().split(" "); + + if (parts.length >= 2) { + if (parts[0].equals("bgload")) { + info.addBackground(parts[1]); + } else if (parts[0].equals("setimg")) { + info.addForeground(parts[1]); + } else if (parts[0].equals("sound")) { + info.addSound(parts[1]); + } else if (parts[0].equals("music")) { + info.addMusic(parts[1]); + } else if (parts[0].equals("jump")) { + info.addJump(parts[1]); + } + } + } + } + + //Getters + + //Setters + protected void setRoute(Map resources, Map nodes, + String filename, int route) + { + if (filename.equals("main.scr") || filename.startsWith("special/")) { + return; + } + + FileInfo info = resources.get(filename); + GraphNode node = nodes.get(filename); + + if (info == null || info.getRoute() == route) { + return; + } + if (info.getRoute() != 0) { + System.err.println(filename+" route="+info.getRoute()+"+"+route); + } + + info.setRoute(route); + for (GraphNode out : node.outgoing) { + setRoute(resources, nodes, out.getName(), route); + } + } + + //Inner Classes + private static class FileInfo { + + private int route; + + private String filename; + private Set background; + private Set foreground; + private Set sound; + private Set music; + private Set jumpsTo; + + public FileInfo(String filename) { + this.filename = filename; + + background = new HashSet(); + foreground = new HashSet(); + sound = new HashSet(); + music = new HashSet(); + jumpsTo = new HashSet(); + } + + //Functions + public void addBackground(String filename) { + if (!filename.startsWith("special/")) { + background.add(filename); + } + } + public void addForeground(String filename) { + if (!filename.startsWith("special/")) { + foreground.add(filename); + } + } + public void addSound(String filename) { + if (!filename.equals("~") && !filename.startsWith("special/")) { + sound.add(filename); + } + } + public void addMusic(String filename) { + if (!filename.equals("~") && !filename.startsWith("special/")) { + music.add(filename); + } + } + public void addJump(String filename) { + if (!filename.startsWith("special/")) { + jumpsTo.add(filename); + } + } + + //Getters + public int getRoute() { return route; } + public String getFilename() { return filename; } + + //Setters + public void setRoute(int r) { this.route = r; } + + //Save Support + public void save(String filename) throws IOException { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("Filename: %s\n", filename)); + + sb.append("\n\njumps:\n"); + for (String s : jumpsTo) { sb.append(s); sb.append('\n'); } + sb.append("\n\nbackgrounds:\n"); + for (String s : background) { sb.append(s); sb.append('\n'); } + sb.append("\n\nforegrounds:\n"); + for (String s : foreground) { sb.append(s); sb.append('\n'); } + sb.append("\n\nsounds & voices:\n"); + for (String s : sound) { sb.append(s); sb.append('\n'); } + sb.append("\n\nmusic:\n"); + for (String s : music) { sb.append(s); sb.append('\n'); } + + FileUtil.write(new File(filename), sb.toString()); + } + } + + private static class GraphNode { + + private String name; + + public Set incoming; + public Set outgoing; + + public GraphNode(String name) { + this.name = name; + + incoming = new HashSet(); + outgoing = new HashSet(); + } + + public String getName() { return name; } + } + + private static class FileHashingTask implements Callable { + + private boolean hash = true; + private String files[]; + private String rootFolder; + private FileMapper nameMapping; + + public FileHashingTask(String files[], String rootFolder, FileMapper nameMapping) { + this.files = files; + this.rootFolder = rootFolder; + this.nameMapping = nameMapping; + } + + @Override + public String call() throws Exception { + StringBuilder sb = new StringBuilder(64 << 10); + for (String file : files) { + try { + String hash = hash(file); + if (hash != null) { + sb.append(hash); + } + } catch (Exception e) { + Log.w("Exception generating hash: " + file, e); + } + } + return sb.toString(); + } + + private String hash(String filename) throws Exception { + try { + int fsize = 0; + String fhash = ""; + if (hash) { + String rname = nameMapping.getOriginal(filename.substring(filename.lastIndexOf('/')+1)); + if (rname == null) { + rname = filename; + } + + File file = new File(rootFolder+"/"+rname); + fsize = (int)file.length(); + fhash = HashUtil.hashToString(HashUtil.generateHash(file)); + } + + filename = filename.replaceAll("background/", "background.zip/background/"); + filename = filename.replaceAll("foreground/", "foreground.zip/foreground/"); + filename = filename.replaceAll("sound/", "sound.zip/sound/"); + filename = filename.replaceAll("script/", "script.zip/script/"); + filename = filename.replaceAll("music/", "sound/"); + + if (hash) { + return String.format("\t\n", filename, fsize, fhash, filename); + } else { + return String.format("\t\n", filename, filename); + } + } catch (FileNotFoundException fnfe) { + //System.err.println(fnfe); + } + + return null; + } + + } + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/RouteParser.java b/UI/src/nl/weeaboo/krkr/fate/RouteParser.java new file mode 100644 index 0000000..807937f --- /dev/null +++ b/UI/src/nl/weeaboo/krkr/fate/RouteParser.java @@ -0,0 +1,265 @@ +package nl.weeaboo.krkr.fate; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.util.Map; + +import nl.weeaboo.common.StringUtil; + +public class RouteParser { + + private FateScriptConverter fateConv; + + public RouteParser(FateScriptConverter fc) { + this.fateConv = fc; + } + + //Functions + public void parse(File file, Map appendMap) { + try { + BufferedReader in = new BufferedReader(new InputStreamReader( + new FileInputStream(file), fateConv.getSourceFileEncoding())); + + String filenameNoExt = StringUtil.stripExtension(file.getName()); + if (!isIncludedInThisRun(filenameNoExt)) { + return; + } + + String text; + while ((text = in.readLine()) != null) { + parse(scenarioFileRename(fateConv, filenameNoExt), text, appendMap); + } + + in.close(); + } catch (Exception e) { + System.err.println(file.getAbsolutePath()); + e.printStackTrace(); + } + } + + protected void parse(String filenameNoExt, String line, Map appendMap) { + if (line.indexOf(';') < 0) { + return; + } + + String primeSplit[] = line.split(";"); + int sceneNum = Integer.parseInt(primeSplit[0]); + String pm[] = primeSplit[1].split("\'"); + + if (pm[0].equals("SCENE")) { + StringBuilder sb = new StringBuilder(); + + //String title = pm[10]; + int numberOfOperations = Integer.parseInt(pm[11]); + + //First do the operations... + String last = pm[12 + numberOfOperations]; + if (!last.equals("0")) { + String pm2[] = last.split(":"); + + int numberOfOperations2 = Integer.parseInt(pm2[0]); + for (int j = 0; j < numberOfOperations2; j++) { + sb.append(new FlowFlag(pm2[1+j], "o").operate()+"\n"); + } + } + + //..and then jump, not the other way around :( + for (int j = 0; j < numberOfOperations; j++) { + String parts[] = pm[12+j].split(":"); + + int numberOfPaths = Integer.parseInt(parts[0]); + int link = Integer.parseInt(parts[numberOfPaths + 1]); //1=Logical AND, 2=Logical OR + int target = Integer.parseInt(parts[numberOfPaths + 2]); + + String jumpStr = "jump " + getScript(filenameNoExt, target) + "\n"; + if (link == 1) { + String spaces = ""; + for (int i = 0; i < numberOfPaths; i++) { + sb.append(spaces); + sb.append("if "); + sb.append(new FlowFlag(parts[1+i], "d").decide()); + sb.append("\n"); + spaces += " "; + } + sb.append(spaces + jumpStr); + for (int i = 0; i < numberOfPaths; i++) { + sb.append(spaces.substring(Math.min(spaces.length(), 2 * (i+1)))); + sb.append("fi\n"); + } + } else { + for (int i = 0; i < numberOfPaths; i++) { + sb.append("if "); + sb.append(new FlowFlag(parts[1+i], "d").decide()); + sb.append("\n"); + sb.append(" " + jumpStr + "\n"); + sb.append("fi\n"); + } + } + } + if (numberOfOperations == 0) { + sb.append("jump main.scr\n"); + } + + appendMap.put(getAppendKey(filenameNoExt, sceneNum), sb.toString()); + } else if (pm[0].equals("SELECTER")) { + //String title = pm[10]; + int numberOfOperations = Integer.parseInt(pm[11]); + + String choices[] = new String[numberOfOperations]; + for (int j = 0; j < numberOfOperations; j++) { + //Choice Display Text|scene number + choices[j] = pm[12 + j*3 + 2] + "|" + getScript(filenameNoExt, Integer.parseInt(pm[12+j*3])); + } + + String string = "jump main.scr\n"; + if (numberOfOperations > 0) { + string = fateConv.createJump(choices); + } + + appendMap.put(getAppendKey(filenameNoExt, sceneNum), string); + } else if (pm[0].equals("OUTERLABEL")) { + //String title = (pm.length > 12 ? pm[12] : ""); + String file = scenarioFileRename(fateConv, pm[10]); + + String jump; + if (file != null) { + int target = Integer.parseInt(pm[11]); + jump = "jump " + getScript(file, target) + "\n"; + } else { + jump = "text \njump special/ulw.scr\n"; + throw new RuntimeException("RouteParser :: Error parsing: " + filenameNoExt + " (" + pm[10] + ")"); + } + appendMap.put(getAppendKey(filenameNoExt, sceneNum), jump); + } + } + + public boolean isIncludedInThisRun(String filename) { + filename = scenarioFileRename(fateConv.getAllowedRoutes(), filename); + + String prefixes[] = new String[] {"prologue", "fate", "ubw", "hf"}; + for (int n = 0; n < fateConv.getAllowedRoutes(); n++) { + if (filename.startsWith(prefixes[n])) { + return true; + } + } + return false; + } + public static String scenarioFileRename(FateScriptConverter fc, String file) { + return scenarioFileRename(fc.getAllowedRoutes(), file); + } + public static String scenarioFileRename(int allowedRoutes, String file) { + file = StringUtil.stripExtension(file); + + String replace[] = new String[] { + "プロローグ", "prologue", + "セイバールート", "fate", + "セイバーエピローグ", "fate-ending", + "凛ルート", "ubw", + "凛エピローグ", "ubw-ending", + "桜ルート", "hf", + "桜エピローグ", "hf-ending", + }; + int L = allowedRoutes; + if (L >= 4) L++; + if (L >= 3) L++; + if (L >= 2) L++; + + int n; + for (n = 0; n < L * 2; n+=2) { + if (file.startsWith(replace[n])) { + String r = scenarioFileRename2(file.substring(replace[n].length(), file.length())); + file = replace[n+1]; + if (r != null) file += r; + return file; + } + } + return file; + } + private static String scenarioFileRename2(String part) { + String replace[] = new String[] { + "1日目", "00", + "2日目", "01", + "3日目", "02", + "一日目", "01", + "二日目", "02", + "三日目", "03", + "四日目", "04", + "五日目", "05", + "六日目", "06", + "七日目", "07", + "八日目", "08", + "九日目", "09", + "十日目", "10", + "十一日目", "11", + "十二日目", "12", + "十三日目", "13", + "十四日目", "14", + "十五日目", "15", + "十六日目", "16" + }; + + for (int n = replace.length-2; n >= 0; n -= 2) { + int index = part.indexOf(replace[n]); + if (index >= 0) { + part = part.substring(0, index) + replace[n+1] + + part.substring(index+replace[n].length()); + } + } + return part; + } + + //Getters + private String getAppendKey(String filenameNoExt, int target) { + return filenameNoExt + "-" + (target<10?"0":"") + target + ".ks"; + } + private String getScript(String filenameNoExt, int target) { + return filenameNoExt + "-" + (target<10?"0":"") + target + ".scr"; + } + + //Setters + + //Inner Classes + private static class FlowFlag { + + String name; + int value0, value1; + + public FlowFlag(String str, String type) { + try { + int pos; + name = str.substring(0, pos = str.indexOf("//")); + str = str.substring(pos+2); + value0 = Integer.parseInt(str.substring(0, pos = str.indexOf("//"))); + str = str.substring(pos+2); + value1 = Integer.parseInt(str); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String decide() { + String operators[] = new String[] {"==", "!=", "<", ">", "<=", ">="}; + return name + " " + operators[value0] + " " + value1; + } + + public String operate() { + String command = "setvar"; + if (name.startsWith("g")) { + command = "gsetvar"; + } + + if (value1 == 1) { //Add + return command + " " + name + " + " + value0; + } else if (value1 == 2) { //Substract + return command + " " + name + " - " + value0; + } else if (value1 == 3) { //Set + return command + " " + name + " = " + value0; + } + return "#Error in FlowFlag.operate()"; + } + + } + +} diff --git a/UI/src/nl/weeaboo/krkr/fate/res/icon.ico b/UI/src/nl/weeaboo/krkr/fate/res/icon.ico new file mode 100644 index 0000000..c5da089 Binary files /dev/null and b/UI/src/nl/weeaboo/krkr/fate/res/icon.ico differ diff --git a/UI/src/nl/weeaboo/krkr/fate/res/icon.png b/UI/src/nl/weeaboo/krkr/fate/res/icon.png new file mode 100644 index 0000000..316ac2d Binary files /dev/null and b/UI/src/nl/weeaboo/krkr/fate/res/icon.png differ diff --git a/UI/src/nl/weeaboo/vnds/AbstractConversionGUI.java b/UI/src/nl/weeaboo/vnds/AbstractConversionGUI.java new file mode 100644 index 0000000..b53260b --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/AbstractConversionGUI.java @@ -0,0 +1,513 @@ +package nl.weeaboo.vnds; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.imageio.ImageIO; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTextArea; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.awt.AwtUtil; +import nl.weeaboo.awt.FileBrowseField; +import nl.weeaboo.awt.IconListCellRenderer; +import nl.weeaboo.awt.Sash; +import nl.weeaboo.common.Dim; +import nl.weeaboo.common.ScaleUtil; +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.tools.ImageConverter.ConvertType; +import nl.weeaboo.vnds.tools.SoundConverter; + +@SuppressWarnings("serial") +public abstract class AbstractConversionGUI extends JFrame implements Runnable { + + protected final String gameFolderName; + protected final boolean hasVoices; + private Dim originalSize; + + private JPanel propertyPanel; + protected final FileBrowseField gameFolderField; + protected final FileBrowseField outputFolderField; + protected final JComboBox targetCombo; + protected final JCheckBox cleanTempFilesButton; + protected final JSpinner threadsSpinner; + protected final JComboBox pngQuantCombo; + protected final JSpinner musicVolumeSpinner; + protected final JSpinner sfxVolumeSpinner; + protected final JCheckBox voiceCheck; + + private final JSpinner jpgQualitySpinner; + private final JLabel mp3AverageBitrateLabel; + private final JSpinner mp3AverageBitrateSpinner; + private final JLabel mp3MaxBitrateLabel; + private final JSpinner mp3MaxBitrateSpinner; + private final JLabel aacQualityLabel; + private final JSpinner aacQualitySpinner; + private final JLabel vorbisQualityLabel; + private final JComboBox vorbisQualityCombo; + + private JTextArea outputField; + private JScrollPane outputFieldScrollPane; + private JButton startButton; + + public AbstractConversionGUI(String title, URL iconURL, File defaultSrcF, File defaultDstF, + String gameFolderName, boolean hasVoices, Dim originalSize) + { + this.gameFolderName = gameFolderName; + this.hasVoices = hasVoices; + this.originalSize = originalSize; + + setTitle(title); + try { + BufferedImage icon = ImageIO.read(iconURL); + AwtUtil.setFrameIcon(this, icon); + } catch (IOException ioe) { + Log.w("Exception settings frame icon", ioe); + } + + outputField = new JTextArea(15, 60); + outputField.setEditable(false); + outputFieldScrollPane = new JScrollPane(outputField); + outputFieldScrollPane.setVisible(false); + + startButton = new JButton("Start"); + startButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (!preConvertCheck(gameFolderField.getFile(), + outputFolderField.getFile())) + { + return; + } + + OutputStream outstream = new OutputStream() { + private final int CHUNKS_LEN_MAX = 128 << 10; + private ArrayDeque chunkLengths = new ArrayDeque(); + private int chunksLengthsTotal = 0; + private ByteBuffer buf = ByteBuffer.allocate(1024); + + public synchronized void write(int b) throws IOException { + buf.put((byte)b); + if (b == '\n' || !buf.hasRemaining()) { + { + final String line = StringUtil.fromUTF8(buf.array(), buf.arrayOffset(), buf.position()); + chunkLengths.add(line.length()); + chunksLengthsTotal += line.length(); + buf.rewind(); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + outputField.append(line); + + JScrollBar vertical = outputFieldScrollPane.getVerticalScrollBar(); + if (vertical != null) { + vertical.setValue(vertical.getMaximum()); + } + } + }); + } + + int removed = 0; + while (chunkLengths.size() > 1 && chunksLengthsTotal > CHUNKS_LEN_MAX) { + removed += chunkLengths.removeFirst(); + } + chunksLengthsTotal -= removed; + + final int endpos = removed; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + outputField.replaceRange("", 0, endpos); + } + }); + } + } + }; + + try { + System.setOut(new PrintStream(outstream, true, "UTF-8")); + System.setErr(new PrintStream(outstream, true, "UTF-8")); + } catch (UnsupportedEncodingException uee) { + Log.w("Changing sysout and syserr not supported", uee); + + System.setOut(new PrintStream(outstream)); + System.setErr(new PrintStream(outstream)); + } + Log.initDefaultHandler(); //Needs to be done since System.err has changed + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + propertyPanel.setVisible(false); + outputFieldScrollPane.setVisible(true); + startButton.setVisible(false); + pack(); + + Thread t = new Thread(AbstractConversionGUI.this); + t.start(); + } + }); + + gameFolderField = FileBrowseField.writeFolder("", defaultSrcF); + outputFolderField = FileBrowseField.writeFolder("", defaultDstF); + + targetCombo = new JComboBox(TargetPlatform.values()); + targetCombo.setSelectedItem(TargetPlatform.NINTENDO_DS); + targetCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + onTargetChanged((TargetPlatform)targetCombo.getSelectedItem()); + } + }); + + pngQuantCombo = new JComboBox(new Object[] { + ConvertType.TYPE_PNG, ConvertType.TYPE_PNG_256_NEUQUANT, ConvertType.TYPE_PNG_256_MEDIAN + }); + pngQuantCombo.setRenderer(new IconListCellRenderer() { + public String getLabelFor(Object object) { + if (object instanceof ConvertType) { + ConvertType ct = (ConvertType)object; + if (ct == ConvertType.TYPE_PNG) return "Full Color (Default)"; + else if (ct == ConvertType.TYPE_PNG_256_NEUQUANT) return "256 Colors (Neuquant)"; + else if (ct == ConvertType.TYPE_PNG_256_MEDIAN) return "256 Colors (Median)"; + } + return null; + } + }); + //pngQuantCombo.setSelectedItem(ConvertType.TYPE_PNG_256_NEUQUANT); + pngQuantCombo.setSelectedItem(ConvertType.TYPE_PNG); + + jpgQualitySpinner = new JSpinner(new SpinnerNumberModel(97, 0, 100, 1)); + mp3AverageBitrateLabel = new JLabel("MP3 Avg Bitrate"); + mp3AverageBitrateSpinner = new JSpinner(new SpinnerNumberModel(64, 16, 128, 16)); + mp3MaxBitrateLabel = new JLabel("MP3 Max Bitrate"); + mp3MaxBitrateSpinner = new JSpinner(new SpinnerNumberModel(96, 16, 128, 16)); + aacQualityLabel = new JLabel("AAC Quality Factor"); + aacQualitySpinner = new JSpinner(new SpinnerNumberModel(SoundConverter.AAC_Q_HIGH, 10, 200, 5)); + vorbisQualityLabel = new JLabel("Vorbis Audio Quality"); + vorbisQualityCombo = new JComboBox(new Object[] {SoundConverter.VORBIS_Q_LOW, SoundConverter.VORBIS_Q_MED, SoundConverter.VORBIS_Q_HIGH}); + vorbisQualityCombo.setSelectedItem(SoundConverter.VORBIS_Q_MED); + vorbisQualityCombo.setRenderer(new IconListCellRenderer() { + public String getLabelFor(Object object) { + if (object instanceof Number) { + int val = ((Number)object).intValue(); + if (val == SoundConverter.VORBIS_Q_LOW) return "Low (-aq " + val + ")"; + if (val == SoundConverter.VORBIS_Q_MED) return "Med (-aq " + val + ")"; + if (val == SoundConverter.VORBIS_Q_HIGH) return "High (-aq " + val + ")"; + } + return super.getLabelFor(object); + } + }); + + musicVolumeSpinner = new JSpinner(new SpinnerNumberModel(100, 10, 500, 25)); + sfxVolumeSpinner = new JSpinner(new SpinnerNumberModel(100, 10, 500, 25)); + + voiceCheck = new JCheckBox(); + voiceCheck.setSelected(hasVoices); + + cleanTempFilesButton = new JCheckBox(); + cleanTempFilesButton.setSelected(true); + threadsSpinner = new JSpinner(new SpinnerNumberModel(Runtime.getRuntime().availableProcessors(), 1, 8, 1)); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setMinimumSize(new Dimension(375, 300)); + setLayout(new BorderLayout()); + } + + //Functions + public void create() { + propertyPanel = createPropertyPanel(); + + add(propertyPanel, BorderLayout.NORTH); + add(outputFieldScrollPane, BorderLayout.CENTER); + add(startButton, BorderLayout.SOUTH); + + onTargetChanged((TargetPlatform)targetCombo.getSelectedItem()); + + pack(); + setLocationRelativeTo(null); + setVisible(true); + } + + protected void onTargetChanged(TargetPlatform platform) { + boolean android = (platform != null && platform.isAndroid()); + + mp3AverageBitrateLabel.setEnabled(!android); + mp3AverageBitrateSpinner.setEnabled(!android); + mp3MaxBitrateLabel.setEnabled(!android); + mp3MaxBitrateSpinner.setEnabled(!android); + aacQualityLabel.setEnabled(!android); + aacQualitySpinner.setEnabled(!android); + vorbisQualityLabel.setEnabled(android); + vorbisQualityCombo.setEnabled(android); + } + + private JPanel createPropertyPanel() { + JPanel panel1 = new JPanel(new GridLayout(-1, 2, 5, 5)); + fillPathsPanel(panel1); + + JPanel settingsPanel = new JPanel(new GridLayout(-1, 2, 5, 5)); + fillSettingsPanel(settingsPanel); + + JPanel converterSettingsPanel = new JPanel(new GridLayout(-1, 2, 5, 5)); + fillConverterSettingsPanel(converterSettingsPanel); + + JPanel otherPanel = new JPanel(new GridLayout(-1, 2, 5, 5)); + otherPanel.add(new JLabel("Clean intermediate files?")); otherPanel.add(cleanTempFilesButton); + otherPanel.add(new JLabel("Threads")); otherPanel.add(threadsSpinner); + + JPanel panel = new JPanel(); + panel.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.add(panel1); + panel.add(Box.createVerticalStrut(10)); + panel.add(new Sash(Sash.HORIZONTAL)); + panel.add(Box.createVerticalStrut(10)); + panel.add(settingsPanel); + panel.add(Box.createVerticalStrut(15)); + panel.add(converterSettingsPanel); + panel.add(Box.createVerticalStrut(10)); + panel.add(new Sash(Sash.HORIZONTAL)); + panel.add(Box.createVerticalStrut(10)); + panel.add(otherPanel); + + JPanel outerPanel = new JPanel(new BorderLayout()); + outerPanel.setBackground(panel.getBackground().darker()); + outerPanel.add(panel); + return outerPanel; + } + + protected void fillPathsPanel(JPanel panel) { + panel.add(new JLabel("Game Folder")); panel.add(gameFolderField); + panel.add(new JLabel("Output Folder")); panel.add(outputFolderField); + } + + protected void fillSettingsPanel(JPanel panel) { + panel.add(new JLabel("Target Platform")); panel.add(targetCombo); + + if (hasVoices) { + panel.add(new JLabel("Enable Voices")); panel.add(voiceCheck); + } + } + + protected void fillConverterSettingsPanel(JPanel panel) { + panel.add(new JLabel("Music Volume")); panel.add(musicVolumeSpinner); + panel.add(new JLabel("SFX Volume")); panel.add(sfxVolumeSpinner); + panel.add(new JLabel("PNG Type")); panel.add(pngQuantCombo); + panel.add(new JLabel("JPG Quality %")); panel.add(jpgQualitySpinner); + panel.add(mp3AverageBitrateLabel); panel.add(mp3AverageBitrateSpinner); + panel.add(mp3MaxBitrateLabel); panel.add(mp3MaxBitrateSpinner); + panel.add(aacQualityLabel); panel.add(aacQualitySpinner); + panel.add(vorbisQualityLabel); panel.add(vorbisQualityCombo); + } + + protected boolean preConvertCheck(File gameFolder, File outputFolder) { + String gameFolderS = outputFolder.getAbsolutePath(); + for (int n = 0; n < gameFolderS.length(); n++) { + if (gameFolderS.charAt(n) > 127) { + JOptionPane.showMessageDialog(AbstractConversionGUI.this, + "Game folder contains non-ASCII characters, conversion may fail", + "Warning", JOptionPane.WARNING_MESSAGE); + break; + } + } + + String outputFolderS = outputFolder.getAbsolutePath(); + for (int n = 0; n < outputFolderS.length(); n++) { + if (outputFolderS.charAt(n) > 127) { + JOptionPane.showMessageDialog(AbstractConversionGUI.this, + "Please use an ASCII-only path (like C:\\temp) for the temp folder", + "Error", JOptionPane.ERROR_MESSAGE); + return false; + } + } + + return true; + } + + @Override + public final void run() { + try { + convert(); + } finally { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + //propertyPanel.setVisible(true); + //outputFieldScrollPane.setVisible(true); + //startButton.setVisible(true); + pack(); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + }); + } + } + + protected void convert() { + File gameFolder = gameFolderField.getFile(); + File outputFolder = outputFolderField.getFile(); + TargetPlatform target = (TargetPlatform)targetCombo.getSelectedItem(); + + convert(gameFolder, outputFolder, target); + } + + protected void convert(File srcFolder, File dstFolder, TargetPlatform target) { + long startTime = System.currentTimeMillis(); + + List list = new ArrayList(); + + Dim imageSize = ScaleUtil.scaleProp(256, 192, target.getWidth(), target.getHeight()); + if (imageSize.w > originalSize.w || imageSize.h > originalSize.h) { + imageSize = ScaleUtil.scaleProp(256, 192, originalSize.w, originalSize.h); + } + + boolean noVoice = !hasVoices || !voiceCheck.isSelected(); + int musicVolume = (Integer)musicVolumeSpinner.getValue(); + int sfxVolume = (Integer)sfxVolumeSpinner.getValue(); + ConvertType pngQuant = (ConvertType)pngQuantCombo.getSelectedItem(); + int jpgQuality = (Integer)jpgQualitySpinner.getValue(); + int mp3_avgb = (Integer)mp3AverageBitrateSpinner.getValue(); + int mp3_minb = 8; + int mp3_maxb = (Integer)mp3MaxBitrateSpinner.getValue(); + int aacQuality = (Integer)aacQualitySpinner.getValue(); + int vorbisQuality = (Integer)vorbisQualityCombo.getSelectedItem(); + + boolean cleanTempFiles = cleanTempFilesButton.isSelected(); + int threads = (Integer)threadsSpinner.getValue(); + + { //Extract data from game install + list.clear(); + + if (originalSize != null) { + list.add("-sourceSize"); + list.add(""+originalSize.w); + list.add(""+originalSize.h); + } + + if (target != null) { + list.add("-targetSize"); + list.add(""+imageSize.w); + list.add(""+imageSize.h); + } + + if (target.isAndroid()) { + list.add("-android"); + } + + if (noVoice) { + list.add("-novoice"); + } + + list.add("-musicvol"); + list.add(""+musicVolume); + + list.add("-sfxvol"); + list.add(""+sfxVolume); + + if (pngQuant != null && pngQuant != ConvertType.TYPE_PNG) { + if (pngQuant == ConvertType.TYPE_PNG_256_MEDIAN) { + list.add("-pngquant"); + list.add("median"); + } else if (pngQuant == ConvertType.TYPE_PNG_256_NEUQUANT) { + list.add("-pngquant"); + list.add("neuquant"); + } else { + Log.w("Unknown PNG quantization type: " + pngQuant); + } + } + + list.add("-jpg"); + list.add(""+jpgQuality); + + list.add("-mp3"); + list.add(""+mp3_avgb); + list.add(""+mp3_minb); + list.add(""+mp3_maxb); + + list.add("-aac"); + list.add(""+aacQuality); + + list.add("-vorbis"); + list.add(""+vorbisQuality); + + list.add("-threads"); + list.add(""+threads); + + callResourceConverter( + getTemplateFolder(gameFolderName).getAbsolutePath(), + srcFolder.getAbsolutePath(), + dstFolder.getAbsolutePath(), list.toArray(new String[0])); + } + + System.gc(); + + { //Convert script + callScriptConverter(new File(dstFolder, "_original").toString(), + new File(dstFolder, "_generated").toString()); + } + + System.gc(); + + { //Create installation package + callPacker(new File(dstFolder, "_generated").toString(), + getPackerTargetFolder(dstFolder, gameFolderName).toString()); + } + + if (cleanTempFiles) { + Log.v("Cleaning temp files..."); + FileUtil.deleteFolder(new File(dstFolder, "_original")); + FileUtil.deleteFolder(new File(dstFolder, "_generated")); + } + + Log.v("========================================"); + Log.v(StringUtil.formatTime(System.currentTimeMillis()-startTime, TimeUnit.MILLISECONDS) + + " Entire conversion finished."); + } + + protected abstract void callResourceConverter(String templateFolder, String srcFolder, + String dstFolder, String... args); + + protected abstract void callScriptConverter(String srcFolder, String dstFolder); + + protected abstract void callPacker(String srcFolder, String dstFolder); + + protected File getTemplateFolder(String gameFolderName) { + return new File("template", gameFolderName); + } + + protected File getPackerTargetFolder(File base, String gameFolderName) { + return new File(base, gameFolderName); + } + + protected Dim getOriginalSize() { + return originalSize; + } + + protected void setOriginalSize(int w, int h) { + originalSize = new Dim(w, h); + } + +} diff --git a/UI/src/nl/weeaboo/vnds/AbstractPacker.java b/UI/src/nl/weeaboo/vnds/AbstractPacker.java new file mode 100644 index 0000000..bc763ee --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/AbstractPacker.java @@ -0,0 +1,242 @@ +package nl.weeaboo.vnds; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileCollectFilter; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.zip.ZipUtil; +import nl.weeaboo.zip.ZipUtil.Compression; + +public abstract class AbstractPacker { + + protected final File srcF; + protected final File dstF; + protected ResourcesUsed resUsed; + + public AbstractPacker(File srcF, File dstF) { + this.srcF = srcF; + this.dstF = dstF; + + resUsed = new ResourcesUsed(); + resUsed.load(srcF, false); + } + + //Functions + + public void pack() { + Log.v("Packing " + dstF.getName() + "..."); + + FileUtil.deleteFolder(dstF); + dstF.mkdirs(); + + Map files = new TreeMap(); + + //Foreground + Log.v("Sprites..."); + final File srcForegroundF = new File(srcF, "foreground"); + final File dstForegroundF = new File(dstF, "foreground"); + dstForegroundF.mkdirs(); + FileUtil.collectFiles(files, srcForegroundF, false, false, new FileCollectFilter() { + public boolean accept(String relpath, File f) { + return f.isDirectory() || relpath.startsWith("special") || resUsed.isForegroundUsed(relpath); + } + }); + resourceZip(new File(dstF, "foreground.zip"), files); + files.clear(); + + //Background + Log.v("Backgrounds..."); + final File srcBackgroundF = new File(srcF, "background"); + final File dstBackgroundF = new File(dstF, "background"); + dstBackgroundF.mkdirs(); + FileUtil.collectFiles(files, srcBackgroundF, false, false, new FileCollectFilter() { + public boolean accept(String relpath, File f) { + return f.isDirectory() || relpath.startsWith("special") || resUsed.isBackgroundUsed(relpath); + } + }); + resourceZip(new File(dstF, "background.zip"), files); + files.clear(); + + //Script + Log.v("Scripts..."); + final File srcScriptF = new File(srcF, "script"); + final File dstScriptF = new File(dstF, "script"); + dstScriptF.mkdirs(); + FileUtil.collectFiles(files, srcScriptF, false, false, new FileCollectFilter() { + public boolean accept(String relpath, File file) { + return file.isDirectory() || relpath.endsWith(".scr"); + } + }); + for (Entry entry : files.entrySet()) { + String relpath = entry.getKey(); + File a = entry.getValue(); + File b = new File(dstScriptF, relpath); + b.getParentFile().mkdirs(); + try { + optimizeScript(relpath, a, b); + } catch (IOException e) { + Log.w("Error copying file: " + a + " to " + b); + } + } + files.clear(); + + FileUtil.collectFiles(files, dstScriptF, false); + resourceZip(new File(dstF, "script.zip"), files); + for (File file : files.values()) { + file.delete(); + } + files.clear(); + + //Sound + Log.v("Music..."); + final File srcSoundF = new File(srcF, "sound"); + final File dstSoundF = new File(dstF, "sound"); + dstSoundF.mkdirs(); + FileUtil.collectFiles(files, srcSoundF, false, false, new FileCollectFilter() { + public boolean accept(String relpath, File f) { + if (f.isDirectory()) return true; + + if (relpath.endsWith(".mp3")) { + //Big fat hack: sneakily copy any music files we encounter, but don't + //include them in the ZIP + tryCopy(srcSoundF, relpath, dstSoundF); + return false; + } + + return relpath.startsWith("special/") + || resUsed.isSoundUsed(relpath) + || resUsed.isMusicUsed(relpath); + } + }); + + Log.v("Sound..."); + resourceZip(new File(dstF, "sound.zip"), files); + files.clear(); + + //Other + tryCopy(srcF, "img.ini", dstF); + tryCopy(srcF, "info.txt", dstF); + tryCopy(srcF, "default.ttf", dstF); + + Map icons = new HashMap(); + FileUtil.collectFiles(icons, srcF, false, false, new FileCollectFilter() { + @Override + public boolean accept(String relpath, File file) { + String fext = StringUtil.getExtension(relpath); + return fext.equalsIgnoreCase("jpg") || fext.equalsIgnoreCase("png"); + } + }); + for (Entry entry : icons.entrySet()) { + tryCopy(srcF, entry.getKey(), dstF); + } + + try { + packMore(); + } catch (IOException e) { + Log.e("Error in packMore()", e); + } + + Log.v("Done"); + } + + protected void packMore() throws IOException { + } + + protected void resourceZip(File dst, Map files) { + String prefix = StringUtil.stripExtension(dst.getName()) + '/'; + + Map newFiles = new HashMap(); + for (Entry entry : files.entrySet()) { + newFiles.put(prefix+entry.getKey(), entry.getValue()); + } + + if (!newFiles.isEmpty()) { + try { + ZipUtil.zip(dst, newFiles, Compression.NONE); + } catch (IOException ioe) { + Log.w("Error zipping file: " + dst, ioe); + } + } + } + + protected void optimizeScript(String relpath, File src, File dst) throws IOException { + if (relpath.equals("main.scr") || relpath.startsWith("special/")) { + FileUtil.copyFile(src, dst); + } else { + StringBuilder sb = new StringBuilder((int)src.length()); + + BufferedReader in = new BufferedReader(new InputStreamReader( + new FileInputStream(src), "UTF-8")); + try { + String line; + while ((line = in.readLine()) != null) { + if (line.startsWith("#") || line.trim().isEmpty()) { + continue; + } + + if (line.startsWith("text ")) { + line = optimizeTextLine(line); + if (line != null && line.length() > 0) { + sb.append(line); + if (!line.endsWith("\n")) { + sb.append('\n'); + } + } + } else { + sb.append(line); + sb.append('\n'); + } + } + } finally { + in.close(); + } + + FileUtil.write(dst, sb.toString()); + } + } + + protected String optimizeTextLine(String line) { + return line.replaceAll("\\s+", " "); + } + + protected static void deleteFiles(Map files) { + for (File file : files.values()) { + if (!file.isDirectory()) { + file.delete(); + } + } + } + + protected static boolean tryCopy(File srcFolder, String relpath, File dstFolder) { + File srcF = new File(srcFolder, relpath); + if (!srcF.exists()) return false; + + File dstF = new File(dstFolder, relpath); + dstF.getParentFile().mkdirs(); + try { + FileUtil.copyFile(srcF, dstF); + return true; + } catch (FileNotFoundException e) { + //Ignore + //Log.w("Error copying file: " + srcF + " to " + dstF); + } catch (IOException e) { + Log.w("Error copying file: " + srcF + " to " + dstF); + } + return false; + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/AbstractResourceConverter.java b/UI/src/nl/weeaboo/vnds/AbstractResourceConverter.java new file mode 100644 index 0000000..6da47ab --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/AbstractResourceConverter.java @@ -0,0 +1,224 @@ +package nl.weeaboo.vnds; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import nl.weeaboo.common.Dim; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.tools.ImageConverter; +import nl.weeaboo.vnds.tools.ImageConverter.ConvertType; +import nl.weeaboo.vnds.tools.ImageConverter.DitheringType; +import nl.weeaboo.vnds.tools.ImageConverter.ScalingType; +import nl.weeaboo.vnds.tools.SoundConverter; + +public abstract class AbstractResourceConverter { + + private boolean dither = true; + private int pngQuant = 0; //1=median, 2=neuquant + private int threads = Runtime.getRuntime().availableProcessors(); + private int jpgQuality = 98; + private int mp3avgb = 48, mp3minb = 8, mp3maxb = 64; + private int aacQuality = SoundConverter.AAC_Q_HIGH; + private int vorbisQuality = SoundConverter.VORBIS_Q_MED; + private int musicVolume = 100; + private int sfxVolume = 100; + + protected boolean convertVoice = true; + protected Dim sourceImageSize = new Dim(800, 600); + protected Dim targetImageSize = new Dim(256, 192); + protected FileExts fileExts = new FileExts(); + + public AbstractResourceConverter() { + } + + //Functions + protected void parseCommandLine(String[] args, int off) throws IOException { + try { + for (int n = off; n < args.length; n++) { + if (args[n].startsWith("-mp3")) { + mp3avgb = Integer.parseInt(args[++n]); + mp3minb = Integer.parseInt(args[++n]); + mp3maxb = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-aac")) { + aacQuality = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-vorbis")) { + vorbisQuality = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-jpg")) { + jpgQuality = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-pngquant")) { + pngQuant = args[++n].equalsIgnoreCase("median") ? 1 : 2; + } else if (args[n].startsWith("-novoice")) { + convertVoice = false; + } else if (args[n].startsWith("-sfxvol")) { + sfxVolume = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-musicvol")) { + musicVolume = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-sourceSize")) { + sourceImageSize = new Dim(Integer.parseInt(args[++n]), Integer.parseInt(args[++n])); + } else if (args[n].startsWith("-targetSize")) { + targetImageSize = new Dim(Integer.parseInt(args[++n]), Integer.parseInt(args[++n])); + } else if (args[n].startsWith("-android")) { + fileExts = FileExts.ANDROID; + dither = false; + } else if (args[n].startsWith("-threads")) { + threads = Integer.parseInt(args[++n]); + } + } + } catch (RuntimeException re) { + throw new IOException(re); + } + } + + protected static void printUsage(Class clazz) { + System.err.printf("Usage: java %s \nflags:" + + "\n\t-musicvol " + + "\n\t-sfxvol " + + "\n\t-sourceSize " + + "\n\t-targetSize " + + "\n\t-novoice" + + "\n\t-android" + + "\n\t-threads " + + + "\n\t-pngquant " + + "\n\t-jpg " + + "\n\t-mp3 " + + "\n\t-aac " + + "\n\t-vorbis " + + "\n", clazz.getName()); + + } + + protected void initOutputFolder(File dstF) throws IOException { + FileUtil.deleteFolder(new File(dstF, "foreground")); + FileUtil.deleteFolder(new File(dstF, "background")); + FileUtil.deleteFolder(new File(dstF, "sound")); + FileUtil.deleteFolder(new File(dstF, "script")); + dstF.mkdirs(); + + FileUtil.write(new File(dstF, "img.ini"), + String.format("width=%d\nheight=%d\n", targetImageSize.w, targetImageSize.h)); + + fileExts.write(new File(dstF, "exts.ini")); + } + + protected BatchProcess createBatch() { + BatchProcess bp = new BatchProcess(); + bp.setTaskSize(100); + bp.setThreads(threads); + bp.setThreadPriority(Thread.MIN_PRIORITY); + bp.addProgressListener(new ProgressListener() { + public void onFinished(String message) { + } + public void onProgress(int value, int max, String message) { + Log.v(String.format("Processing (%d/%d) %s", value, max, message)); + } + }); + + return bp; + } + + private ImageConverter createImageConverter() { + ImageConverter ic = new ImageConverter(); + ic.setMaxThreads(threads); + ic.setQuality(jpgQuality); + ic.setSourceScreenSize(sourceImageSize.w, sourceImageSize.h); + ic.setTargetScreenSize(targetImageSize.w, targetImageSize.h); + return ic; + } + protected ImageConverter createBackgroundConverter() { + ImageConverter ic = createImageConverter(); + if (dither) { + ic.setDitheringType(ImageConverter.DitheringType.FLOYD_STEINBERG); + } else { + ic.setDitheringType(DitheringType.NONE); + } + ic.setScalingType(ScalingType.BACKGROUND); + ic.setMode(ImageConverter.ConvertType.TYPE_JPG); + return ic; + } + protected ImageConverter createForegroundConverter() { + ImageConverter ic = createImageConverter(); + ic.setDitheringType(DitheringType.NONE); + ic.setScalingType(ScalingType.NONE); //ic.setScalingType(ScalingType.SPRITE); + ic.setMode(ConvertType.TYPE_PNG); + if (pngQuant == 1) { + ic.setMode(ConvertType.TYPE_PNG_256_MEDIAN); + } else if (pngQuant == 2) { + ic.setMode(ConvertType.TYPE_PNG_256_NEUQUANT); + } + return ic; + } + + private SoundConverter createSoundEncoder() { + SoundConverter sc = new SoundConverter(); + sc.setMaxThreads(threads); + sc.setAacQuality(aacQuality); + sc.setMp3Quality(mp3minb, mp3maxb, mp3avgb); + sc.setVorbisQuality(vorbisQuality); + return sc; + } + protected SoundConverter createMusicEncoder() { + SoundConverter sc = createSoundEncoder(); + sc.setVolume(musicVolume); + sc.setMode(SoundConverter.ConvertType.fromExt(fileExts.music)); + return sc; + } + protected SoundConverter createSFXEncoder() { + SoundConverter sc = createSoundEncoder(); + sc.setVolume(sfxVolume); + sc.setMode(SoundConverter.ConvertType.fromExt(fileExts.sound)); + return sc; + } + protected SoundConverter createVoiceEncoder() { + SoundConverter sc = createSoundEncoder(); + sc.setVolume(sfxVolume); + sc.setMode(SoundConverter.ConvertType.fromExt(fileExts.voice)); + return sc; + } + + protected void copyTemplate(File templateF, File dstF) throws IOException { + FileUtil.copyFolderContents(templateF, dstF, false, null); + + Map scripts = new HashMap(); + FileUtil.collectFiles(scripts, new File(dstF, "script"), false); + for (File f : scripts.values()) { + fixScriptFileExts(f); + } + scaleSpecialImages(dstF); + } + + protected void fixScriptFileExts(File file) throws IOException { + String text = FileUtil.read(file); + text = text.replaceAll("music ([^\\.\\s]+)\\.\\S+", "music $1." + fileExts.music); + text = text.replaceAll("sound ([^\\.\\s]+)\\.\\S+", "sound $1." + fileExts.sound); + FileUtil.write(file, text); + } + + protected void scaleSpecialImages(File root) { + ImageConverter ic = createForegroundConverter(); + + Map files = new HashMap(); + FileUtil.collectFiles(files, new File(root, "background/special"), false, false, false); + ic.setScalingType(ScalingType.STRETCH); + for (File file : files.values()) { + ic.setMode(file.getName().endsWith(".jpg") ? ConvertType.TYPE_JPG : ConvertType.TYPE_PNG); + ic.convertFile(file); + } + files.clear(); + + FileUtil.collectFiles(files, new File(root, "foreground/special"), false, false, false); + ic.setScalingType(ScalingType.SPRITE); + for (File file : files.values()) { + ic.setMode(file.getName().endsWith(".jpg") ? ConvertType.TYPE_JPG : ConvertType.TYPE_PNG); + ic.convertFile(file); + } + files.clear(); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/BatchProcess.java b/UI/src/nl/weeaboo/vnds/BatchProcess.java new file mode 100644 index 0000000..0334e67 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/BatchProcess.java @@ -0,0 +1,169 @@ +package nl.weeaboo.vnds; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public class BatchProcess { + + private int threads; + private int threadPriority; + private int taskSize; + private List listeners; + + private ExecutorService executor; + private volatile int progress; + private int maxProgress; + + public BatchProcess() { + this(1, 8); + } + public BatchProcess(int threads, int tasksize) { + this.threads = threads; + this.threadPriority = Thread.NORM_PRIORITY; + this.taskSize = tasksize; + this.listeners = new ArrayList(2); + } + + //Functions + public void addProgressListener(ProgressListener pl) { + listeners.add(pl); + } + public void removeProgressListener(ProgressListener pl) { + listeners.remove(pl); + } + + public void run(Map fileMap, FileOp op) throws InterruptedException { + start(fileMap, op); + stop(); + } + + public void start(Map fileMap, FileOp op) { + try { + stop(); + } catch (InterruptedException e) { + + } + + executor = Executors.newFixedThreadPool(threads, new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(threadPriority); + return t; + } + }); + + progress = 0; + maxProgress = fileMap.size(); + + int stepSize = taskSize; + if (stepSize * threads > fileMap.size()) { + stepSize = Math.max(1, fileMap.size() / threads); + } + + ProgressListener ls[] = listeners.toArray(new ProgressListener[listeners.size()]); + + Iterator> i = fileMap.entrySet().iterator(); + while (i.hasNext()) { + final String relpaths[] = new String[stepSize]; + final File files[] = new File[stepSize]; + + int t = 0; + while (t < stepSize && i.hasNext()) { + Entry entry = i.next(); + relpaths[t] = entry.getKey(); + files[t] = entry.getValue(); + t++; + } + + executor.submit(new Task(op, relpaths, files, t, ls)); + } + } + + public void stop() throws InterruptedException { + if (executor != null) { + executor.shutdown(); + executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + + ProgressListener ls[] = listeners.toArray(new ProgressListener[listeners.size()]); + for (ProgressListener pl : ls) { + pl.onFinished("Finished"); + } + + executor = null; + } + } + + //Getters + public int getThreads() { return threads; } + public int getThreadPriority() { return threadPriority; } + public int getTaskSize() { return taskSize; } + + //Setters + public void setThreads(int t) { + if (t <= 0) { + t = Runtime.getRuntime().availableProcessors(); + } + threads = t; + } + public void setThreadPriority(int p) { + threadPriority = p; + } + public void setTaskSize(int ts) { + taskSize = ts; + } + + //Inner Classes + private class Task implements Callable { + + private final FileOp fop; + private final String relpaths[]; + private final File files[]; + private final int len; + private final ProgressListener listeners[]; + + public Task(FileOp op, String rps[], File fs[], int t, ProgressListener pls[]) { + fop = op; + relpaths = rps; + files = fs; + len = t; + listeners = pls; + } + + @Override + public Integer call() throws Exception { + int t = 0; + try { + while (t < len) { + try { + fop.execute(relpaths[t], files[t]); + } catch (Throwable e) { + Log.w("Uncaught exception processing: " + relpaths[t], e); + //throw e; + } + t++; + } + } finally { + synchronized (BatchProcess.this) { + progress += len; + if (listeners.length > 0 && len > 0) { + for (ProgressListener pl : listeners) { + pl.onProgress(progress, maxProgress, ""); + } + } + } + } + return t; + } + + } + +} diff --git a/UI/src/nl/weeaboo/vnds/FileExts.java b/UI/src/nl/weeaboo/vnds/FileExts.java new file mode 100644 index 0000000..b2a61f1 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/FileExts.java @@ -0,0 +1,47 @@ +package nl.weeaboo.vnds; + +import java.io.File; +import java.io.IOException; + +import nl.weeaboo.settings.INIFile; + +public final class FileExts { + + private static String DEFAULT_MUSIC = "mp3"; + private static String DEFAULT_SOUND = "aac"; + private static String DEFAULT_VOICE = "aac"; + + public static final FileExts ANDROID = new FileExts("ogg", "ogg", "ogg"); + + public final String music; + public final String sound; + public final String voice; + + public FileExts() { + this(DEFAULT_MUSIC, DEFAULT_SOUND, DEFAULT_VOICE); + } + public FileExts(String m, String s, String v) { + this.music = m; + this.sound = s; + this.voice = v; + } + + public static FileExts fromFile(File file) throws IOException { + INIFile iniFile = new INIFile(); + iniFile.read(file); + return new FileExts( + iniFile.getString("music", DEFAULT_MUSIC), + iniFile.getString("sound", DEFAULT_SOUND), + iniFile.getString("voice", DEFAULT_VOICE) + ); + } + + public void write(File file) throws IOException { + INIFile iniFile = new INIFile(); + iniFile.put("music", music); + iniFile.put("sound", sound); + iniFile.put("voice", voice); + iniFile.write(file); + } + +} diff --git a/UI/src/nl/weeaboo/vnds/FileMapper.java b/UI/src/nl/weeaboo/vnds/FileMapper.java new file mode 100644 index 0000000..00560c7 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/FileMapper.java @@ -0,0 +1,134 @@ +package nl.weeaboo.vnds; + +import static nl.weeaboo.string.StringUtil2.*; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; + +public class FileMapper implements Iterable> { + + private Map hash2file; + private Map file2hash; + + public FileMapper() { + hash2file = new Hashtable(); + file2hash = new Hashtable(); + } + + //Functions + public String add(String filename) { + return add(filename, ""); + } + public String add(String filename, String hashPrefix) { + return add(filename, hashPrefix, StringUtil.getExtension(filename)); + } + public String add(String key, String hashPrefix, String fext) { + key = key.toLowerCase(); + String hash = getHashed(key); + if (hash != null) return hash; + + Random random = new Random(key.hashCode()); + + String ext = "." + fext; + String shortname = getShortname(key); + if (shortname != null) { + hash = (hashPrefix + shortname + ext).toLowerCase(); + } + + while (hash == null || hash2file.containsKey(hash)) { + hash = (hashPrefix + getRandomString(5, random) + ext).toLowerCase(); + }; + + hash2file.put(hash, key); + file2hash.put(key, hash); + return hash; + } + public String getHashed(String original) { + return file2hash.get(original.toLowerCase()); + } + public String getOriginal(String hashed) { + return hash2file.get(hashed.toLowerCase()); + } + + private String getShortname(String filename) { + filename = StringUtil.stripExtension(filename.substring(filename.lastIndexOf('/')+1)); + filename = filename.replaceAll("[?\\[\\]/\\=+<>:;\",*| ]", "_"); + + //Remove double file extensions + int index = filename.indexOf('.'); + if (index > 0) { + filename = filename.substring(0, index); + } + + //Truncate long filenames + if (filename.length() > 12) { + filename = filename.substring(0, 12); + } + + //Return null if the filename is invalid + if (!FileUtil.isValidFilename(filename)) { + return null; + } + + //Return null if the filename is non-ascii + for (int n = 0; n < filename.length(); n++) { + if (filename.charAt(n) > 127) { + return null; + } + } + + return filename; + } + + public void load(String file) throws IOException { + try { + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8")); + String line; + while ((line = in.readLine()) != null) { + String split[] = line.split(" > "); + hash2file.put(split[0], split[1]); + file2hash.put(split[1], split[0]); + } + in.close(); + } catch (FileNotFoundException fnfe) { + } + } + public void save(String file) throws IOException { + OutputStream fout = new BufferedOutputStream(new FileOutputStream(file)); + for (Entry entry : hash2file.entrySet()) { + fout.write(entry.getKey().getBytes("UTF-8")); + fout.write(" > ".getBytes("UTF-8")); + fout.write(entry.getValue().getBytes("UTF-8")); + fout.write('\n'); + } + fout.close(); + } + + public void put(String hash, String original) { + hash2file.put(hash, original); + file2hash.put(original, hash); + } + + /** Returns the entries in form */ + public Iterator> iterator() { + return hash2file.entrySet().iterator(); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/FileOp.java b/UI/src/nl/weeaboo/vnds/FileOp.java new file mode 100644 index 0000000..362df68 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/FileOp.java @@ -0,0 +1,10 @@ +package nl.weeaboo.vnds; + +import java.io.File; +import java.io.IOException; + +public interface FileOp { + + public void execute(String relpath, File file) throws IOException; + +} diff --git a/UI/src/nl/weeaboo/vnds/HashUtil.java b/UI/src/nl/weeaboo/vnds/HashUtil.java new file mode 100644 index 0000000..642ce2a --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/HashUtil.java @@ -0,0 +1,63 @@ +package nl.weeaboo.vnds; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class HashUtil { + + public static String generateHash(String string) throws NoSuchAlgorithmException, IOException { + return hashToString(generateHash(string.getBytes("UTF-8"))); + } + public static byte[] generateHash(File file) throws IOException, NoSuchAlgorithmException { + InputStream in = new BufferedInputStream(new FileInputStream(file)); + byte hash[] = null; + + try { + hash = generateHash(in); + } finally { + in.close(); + } + + return hash; + } + public static byte[] generateHash(InputStream in) throws IOException, NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + + byte buffer[] = new byte[16 * 1024]; + while (true) { + int read = in.read(buffer, 0, buffer.length); + if (read == -1) { + break; + } + md.update(buffer, 0, read); + } + + return md.digest(); + } + + public static byte[] generateHash(byte data[]) throws NoSuchAlgorithmException { + return generateHash(data, 0, data.length); + } + public static byte[] generateHash(byte data[], int offset, int length) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(data, offset, length); + return md.digest(); + } + + public static String hashToString(byte hash[]) { + StringBuilder sb = new StringBuilder(); + for (byte b : hash) { + int nibble1 = (b & 0xF0) >> 4; + int nibble2 = b & 0x0F; + sb.append(Integer.toHexString(nibble1)); + sb.append(Integer.toHexString(nibble2)); + } + return sb.toString(); + } + +} \ No newline at end of file diff --git a/UI/src/nl/weeaboo/vnds/Log.java b/UI/src/nl/weeaboo/vnds/Log.java new file mode 100644 index 0000000..719dadc --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/Log.java @@ -0,0 +1,85 @@ +package nl.weeaboo.vnds; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +public final class Log { + + private static final Logger INSTANCE; + private static Handler handler; + + static { + INSTANCE = Logger.getLogger("nl.weeaboo.vnds"); + INSTANCE.setUseParentHandlers(false); + INSTANCE.setLevel(Level.CONFIG); + + initDefaultHandler(); + } + + public static Logger getLogger() { + return INSTANCE; + } + + public static void initDefaultHandler() { + if (handler != null) { + INSTANCE.removeHandler(handler); + } + + handler = new ConsoleHandler(); + handler.setLevel(Level.ALL); + handler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + String msg = record.getMessage(); + if (msg != null) { + pw.println(msg); + } + + Throwable t = record.getThrown(); + if (t != null) { + t.printStackTrace(pw); + } + return sw.toString(); + } + }); + INSTANCE.addHandler(handler); + } + + public static void v(String message) { + v(message, null); + } + public static void v(String message, Throwable t) { + INSTANCE.logp(Level.CONFIG, null, null, message, t); + } + + public static void fnf(String message) { + fnf(message, null); + } + public static void fnf(String message, Throwable t) { + INSTANCE.logp(Level.INFO, null, null, "FileNotFound: " + message, t); + } + + public static void w(String message) { + w(message, null); + } + public static void w(String message, Throwable t) { + INSTANCE.logp(Level.WARNING, null, null, message, t); + } + + public static void e(String message) { + e(message, null); + } + public static void e(String message, Throwable t) { + INSTANCE.logp(Level.SEVERE, null, null, message, t); + } + +} diff --git a/UI/src/nl/weeaboo/vnds/Patcher.java b/UI/src/nl/weeaboo/vnds/Patcher.java new file mode 100644 index 0000000..d9fa4db --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/Patcher.java @@ -0,0 +1,21 @@ +package nl.weeaboo.vnds; + +import java.util.Map; + +public class Patcher { + + protected static final String RM_TEXT = "#removed by patcher"; + + //Functions + public void fillAppendMap(Map appendMap) { + } + public void patchPre(Map> patch) { + } + public void patchPost(Map> patch) { + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/ProgressListener.java b/UI/src/nl/weeaboo/vnds/ProgressListener.java new file mode 100644 index 0000000..b6fd5c4 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/ProgressListener.java @@ -0,0 +1,18 @@ +package nl.weeaboo.vnds; + +public interface ProgressListener { + + //Functions + + /** + * @param max Maximum progress, -1 if unknown. + */ + public void onProgress(int value, int max, String message); + + public void onFinished(String message); + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/ProgressRunnable.java b/UI/src/nl/weeaboo/vnds/ProgressRunnable.java new file mode 100644 index 0000000..738e71b --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/ProgressRunnable.java @@ -0,0 +1,12 @@ +package nl.weeaboo.vnds; + +public interface ProgressRunnable { + + //Functions + public void run(ProgressListener pl); + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/ResourcesUsed.java b/UI/src/nl/weeaboo/vnds/ResourcesUsed.java new file mode 100644 index 0000000..082c683 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/ResourcesUsed.java @@ -0,0 +1,106 @@ +package nl.weeaboo.vnds; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Set; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; + +public class ResourcesUsed { + + private static final String FN_FOREGROUND = "resused-foreground.txt"; + private static final String FN_BACKGROUND = "resused-background.txt"; + private static final String FN_SOUND = "resused-sound.txt"; + private static final String FN_MUSIC = "resused-music.txt"; + + private Set foreground; + private Set background; + private Set sound; + private Set music; + + public ResourcesUsed() { + foreground = new HashSet(); + background = new HashSet(); + sound = new HashSet(); + music = new HashSet(); + } + + //Functions + public void load(File folder, boolean suppressWarnings) { + load(foreground, new File(folder, FN_FOREGROUND), suppressWarnings); + load(background, new File(folder, FN_BACKGROUND), suppressWarnings); + load(sound, new File(folder, FN_SOUND), suppressWarnings); + load(music, new File(folder, FN_MUSIC), suppressWarnings); + } + + protected static boolean load(Set out, File file, boolean suppressWarnings) { + try { + for (String line : FileUtil.read(file).split("(\\\r)?\\\n")) { + out.add(line.trim()); + } + return true; + } catch (IOException ioe) { + if (!suppressWarnings) { + Log.w("Exception reading: " + file, ioe); + } + return false; + } + } + + public void save(File folder) { + folder.mkdirs(); + save(foreground, new File(folder, FN_FOREGROUND)); + save(background, new File(folder, FN_BACKGROUND)); + save(sound, new File(folder, FN_SOUND)); + save(music, new File(folder, FN_MUSIC)); + } + + protected static boolean save(Set set, File file) { + try { + PrintWriter pout = new PrintWriter(file, "UTF-8"); + try { + for (String line : set) { + pout.println(line); + } + } finally { + pout.close(); + } + return true; + } catch (IOException ioe) { + Log.w("Exception writing: " + file, ioe); + return false; + } + } + + //Getters + public boolean isForegroundUsed(String filename) { + return foreground.contains(filename); + } + public boolean isBackgroundUsed(String filename) { + return background.contains(filename); + } + public boolean isSoundUsed(String filename) { + return sound.contains(filename); + } + public boolean isMusicUsed(String filename) { + return music.contains(filename); + } + + //Setters + public void setForegroundUsed(String filename) { + foreground.add(filename); + } + public void setBackgroundUsed(String filename) { + background.add(filename); + } + public void setSoundUsed(String filename) { + sound.add(filename); + } + public void setMusicUsed(String filename) { + music.add(filename); + } + +} diff --git a/UI/src/nl/weeaboo/vnds/TargetPlatform.java b/UI/src/nl/weeaboo/vnds/TargetPlatform.java new file mode 100644 index 0000000..416ce30 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/TargetPlatform.java @@ -0,0 +1,29 @@ +package nl.weeaboo.vnds; + +public enum TargetPlatform { + NINTENDO_DS("Nintendo DS", 256, 192, false), + ANDROID_QVGA("Android QVGA", 320, 240, true), + ANDROID_HVGA("Android HVGA", 480, 320, true), + ANDROID_WVGA("Android WVGA", 800, 480, true), + ANDROID_WSVGA("Android WSVGA", 1024, 600, true), + ANDROID_WXGA("Android WXGA", 1280, 800, true); + + private final String label; + private final int w, h; + private final boolean android; + + private TargetPlatform(String label, int w, int h, boolean android) { + this.label = label; + this.w = w; + this.h = h; + this.android = android; + } + + public int getWidth() { return w; } + public int getHeight() { return h; } + public boolean isAndroid() { return android; } + + public String toString() { + return String.format("%s (%dx%d)", label, w, h); + } +} diff --git a/UI/src/nl/weeaboo/vnds/VNDSProgressDialog.java b/UI/src/nl/weeaboo/vnds/VNDSProgressDialog.java new file mode 100644 index 0000000..e46184e --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/VNDSProgressDialog.java @@ -0,0 +1,90 @@ +package nl.weeaboo.vnds; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.awt.AwtUtil; + +public class VNDSProgressDialog { + + private JPanel panel; + private JLabel messageLabel; + private JProgressBar progressBar; + + public VNDSProgressDialog() { + messageLabel = new JLabel("Initializing..."); + progressBar = new JProgressBar(); + + panel = new JPanel(new BorderLayout(5, 5)); + panel.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel.add(messageLabel, BorderLayout.CENTER); + panel.add(progressBar, BorderLayout.SOUTH); + } + + public void showDialog(final ProgressRunnable task, final ProgressListener pl) { + final JDialog dialog = new JDialog(); + dialog.setResizable(false); + dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); + dialog.setAlwaysOnTop(true); + + panel.setPreferredSize(new Dimension(400, 60)); + dialog.add(panel); + dialog.pack(); + dialog.setLocationRelativeTo(null); + dialog.setVisible(true); + + final ProgressListener progressListener = new ProgressListener() { + public void onProgress(final int value, final int max, final String message) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progressBar.setMaximum(max); + progressBar.setValue(value); + messageLabel.setText(message); + + if (pl != null) { + pl.onProgress(value, max, message); + } + } + }); + } + public void onFinished(final String message) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progressBar.setValue(progressBar.getMaximum()); + + if (pl != null) { + pl.onFinished(message); + } + } + }); + } + }; + + Thread t = new Thread(new Runnable() { + public void run() { + try { + ((ProgressRunnable)task).run(progressListener); + } catch (Exception e) { + AwtUtil.showError(e); + } + SwingUtilities.invokeLater(new Runnable() { + public void run() { + dialog.dispose(); + } + }); + } + }); + t.start(); + } + + //Functions + //Getters + //Setters +} diff --git a/UI/src/nl/weeaboo/vnds/VNImageUtil.java b/UI/src/nl/weeaboo/vnds/VNImageUtil.java new file mode 100644 index 0000000..8f03269 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/VNImageUtil.java @@ -0,0 +1,124 @@ +package nl.weeaboo.vnds; + +import java.awt.Insets; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; + +import javax.imageio.ImageIO; + +import nl.weeaboo.awt.ImageUtil; +import nl.weeaboo.image.ImageInfo; +import nl.weeaboo.image.ImageInfo.Result; + +public final class VNImageUtil { + + private VNImageUtil() { + } + + public static int checkHugeImage(File file) throws IOException { + ImageInfo ii = new ImageInfo(); + Result r = null; + try { + r = ii.readHeader(file); + } catch (IOException ioe) { + //Ignore + } + + if (r == null) { + return -1; + } + + if (r.width > 4096 || r.height > 4096) { + String msg = String.format("Detected huge image (%d x %d) %s", + r.width, r.height, file.getAbsolutePath()); + Log.w(msg); + return 1; + } + return 0; + } + + public static BufferedImage readBMP(File file) throws IOException { + final int headerSize = 0x36; + if (file.length() <= headerSize) { + return ImageIO.read(file); + } + + int bpp = 0; + + //Convert 32-bit bitmaps to a more sensible format + FileInputStream fin = new FileInputStream(file); + FileChannel fc = fin.getChannel(); + try { + fc.position(0x12); + byte temp[] = new byte[4]; + fin.read(temp); + int width = ((temp[3]&0xFF)<<24)|((temp[2]&0xFF)<<16)|((temp[1]&0xFF)<<8)|(temp[0]&0xFF); + fin.read(temp); + int height = ((temp[3]&0xFF)<<24)|((temp[2]&0xFF)<<16)|((temp[1]&0xFF)<<8)|(temp[0]&0xFF); + fc.position(fc.position()+2); + fin.read(temp); + bpp = ((temp[3]&0xFF)<<24)|((temp[2]&0xFF)<<16)|((temp[1]&0xFF)<<8)|(temp[0]&0xFF); + + if (bpp != 32) { + fc.position(0); + return ImageIO.read(new BufferedInputStream(fin)); + } + + fc.position(headerSize); + BufferedInputStream bin = new BufferedInputStream(fin, 4096); + int argb[] = new int[width*height]; + for (int y = height-1; y >= 0; y--) { + int t = width * y; + for (int x = 0; x < width; x++) { + bin.read(temp); + argb[t++] = ((temp[3]&0xFF)<<24)|((temp[2]&0xFF)<<16)|((temp[1]&0xFF)<<8)|(temp[0]&0xFF); + } + } + + return ImageUtil.createBufferedImage(width, height, argb, false); + } catch (EOFException eof) { + Log.w("Unreadable bitmap (filename="+file.getName()+", bpp="+bpp+")"); + throw eof; + } catch (IOException ioe) { + throw ioe; + } finally { + fc.close(); + fin.close(); + } + } + + protected static boolean rowEmpty(int argb[], int iw, int ih, int y) { + int offset = y * iw; + for (int x = 0; x < iw; x++) { + if (((argb[offset]>>24) & 0xFF) >= 16) { + return false; + } + offset++; + } + return true; + } + + protected static boolean colEmpty(int argb[], int iw, int ih, int x) { + int offset = x; + for (int y = 0; y < ih; y++) { + if (((argb[offset]>>24) & 0xFF) >= 16) { + return false; + } + offset += iw; + } + return true; + } + + public static void calculateImageInsets(int[] argb, int iw, int ih, Insets i) { + for (int x = 0; x < iw && colEmpty(argb, iw, ih, x); x++) i.left++; + for (int x = iw-1; x >= 0 && colEmpty(argb, iw, ih, x); x--) i.right++; + for (int y = 0; y < ih && rowEmpty(argb, iw, ih, y); y++) i.top++; + for (int y = ih-1; y >= 0 && rowEmpty(argb, iw, ih, y); y--) i.bottom++; + } + +} diff --git a/UI/src/nl/weeaboo/vnds/VNSoundUtil.java b/UI/src/nl/weeaboo/vnds/VNSoundUtil.java new file mode 100644 index 0000000..ce99617 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/VNSoundUtil.java @@ -0,0 +1,158 @@ +package nl.weeaboo.vnds; + +import static nl.weeaboo.common.StringUtil.stripExtension; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.system.ProcessUtil; + +public final class VNSoundUtil { + + + public static String ffmpeg = "ffmpeg"; + + private VNSoundUtil() { + } + + public static File padAudioFile(File src, File dst) throws IOException { + File tempF = new File(dst.getParent(), stripExtension(dst.getName())+".temp"); + tempF.getParentFile().mkdirs(); + + try { + //Decode to raw PCM + + Process p = null; + + redo_transcode: try { + p = ProcessUtil.execInDir(String.format( + "%s -y -i \"%s\" -f s16le -acodec pcm_s16le -ac 2 -ar 44100 \"%s\"", + ffmpeg, src.getAbsolutePath(), tempF.getAbsolutePath()), + "."); + } + catch (Exception e) { + ffmpeg = "avconv"; + break redo_transcode; + } + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + + { + byte[] bytes = FileUtil.readBytes(tempF); + boolean isShort = bytes.length <= 10 * (44100*2*2); + float fadeSeconds = (isShort ? 1 : 3); + float padSeconds = (isShort ? 3 : 0); + + ShortBuffer[] samples = deinterleave(bytes, 2); + fadeInOut(samples, Math.round(fadeSeconds * 44100), + Math.round(fadeSeconds * 44100)); + bytes = interleave(samples); + + //Pad raw PCM with data + FileOutputStream fout = new FileOutputStream(tempF); + try { + fout.write(bytes); + fout.write(new byte[Math.round(padSeconds * 44100 * 2 * 2)]); + fout.flush(); + } finally { + fout.close(); + } + } + + //Encode to target format + p = ProcessUtil.execInDir(String.format( + "ffmpeg -y -f s16le -acodec pcm_s16le -ac 2 -ar 44100 -i \"%s\" \"%s\"", + tempF.getAbsolutePath(), dst.getAbsolutePath()), + "tools"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + } finally { + tempF.delete(); + } + + return dst; + } + + public static void fadeInOut(ShortBuffer[] channels, int inDuration, int outDuration) { + for (ShortBuffer ch : channels) { + inDuration = Math.min(ch.limit()/2, inDuration); + } + + for (ShortBuffer ch : channels) { + outDuration = Math.min(ch.limit()-inDuration, outDuration); + } + + for (ShortBuffer ch : channels) { + int a = 0; + for (int n = 0; n < inDuration; n++) { + float frac = n / (float)inDuration; + ch.put(a, (short)Math.round(frac * ch.get(a))); + a++; + } + + int b = ch.limit()-1; + for (int n = 0; n < outDuration; n++) { + float frac = n / (float)outDuration; + ch.put(b, (short)Math.round(frac * ch.get(b))); + b--; + } + } + } + + protected static ShortBuffer[] deinterleave(byte[] srcArray, int channels) { + ByteBuffer src = ByteBuffer.wrap(srcArray); + src.order(ByteOrder.LITTLE_ENDIAN); + ShortBuffer ssrc = src.asShortBuffer(); + + ShortBuffer[] dst = new ShortBuffer[channels]; + for (int n = 0; n < channels; n++) { + dst[n] = ShortBuffer.allocate(ssrc.limit() / channels); + } + + while (ssrc.remaining() > 0) { + for (ShortBuffer buf : dst) { + buf.put(ssrc.get()); + } + } + + for (ShortBuffer buf : dst) { + buf.rewind(); + } + + return dst; + } + + protected static byte[] interleave(ShortBuffer[] src) { + int len = 0; + int[] oldpos = new int[src.length]; + for (int n = 0; n < oldpos.length; n++) { + oldpos[n] = src[n].position(); + len += src[n].limit(); + } + + ByteBuffer dst = ByteBuffer.allocate(2 * len); + dst.order(ByteOrder.LITTLE_ENDIAN); + ShortBuffer sdst = dst.asShortBuffer(); + + try { + while (sdst.remaining() > 0) { + for (ShortBuffer buf : src) { + sdst.put(buf.get()); + } + } + } finally { + for (int n = 0; n < oldpos.length; n++) { + src[n].position(oldpos[n]); + } + } + dst.rewind(); + + return dst.array(); + } + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/AddCommand.java b/UI/src/nl/weeaboo/vnds/installer/AddCommand.java new file mode 100644 index 0000000..5c70e3d --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/AddCommand.java @@ -0,0 +1,42 @@ +package nl.weeaboo.vnds.installer; + +import java.io.File; +import java.io.IOException; + +import nl.weeaboo.vnds.Log; +import nl.weeaboo.xml.XmlElement; +import nl.weeaboo.xml.XmlReader; + +import org.xml.sax.SAXException; + +public class AddCommand extends PackCommand { + + private String xmlPath; + + public AddCommand(String xmlPath, String outputFolder) { + super(outputFolder); + + this.xmlPath = xmlPath; + } + + //Functions + public void execute() { + try { + File file = new File(xmlPath); + XmlReader xmlReader = new XmlReader(); + XmlElement componentE = xmlReader.read(file).getChild("component"); + + Component c = Component.fromXml(file.getParentFile().getAbsolutePath(), componentE); + c.save(new File(getOutputFolder(), file.getName())); + } catch (SAXException e) { + Log.e("Exception executing add command: " + xmlPath, e); + } catch (IOException ioe) { + Log.e("Exception executing add command: " + xmlPath, ioe); + } + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/Component.java b/UI/src/nl/weeaboo/vnds/installer/Component.java new file mode 100644 index 0000000..7906a18 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/Component.java @@ -0,0 +1,86 @@ +package nl.weeaboo.vnds.installer; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.xml.XmlElement; + +public class Component { + + private String name; + private String desc; + private Set files; + + public Component(String name, String desc) { + this.name = name; + this.desc = desc; + + files = new HashSet(); + } + + //Functions + public void addFile(FileListEntry file) { + files.add(file); + } + public void removeFile(FileListEntry file) { + files.remove(file); + } + + //Getters + public Set getFiles() { + return files; + } + public String getName() { + return name; + } + public String getDesc() { + return desc; + } + + //Setters + + //Save Support + public void save(File file) throws IOException { + XmlElement componentE = new XmlElement("component"); + componentE.addAttribute("name", name); + componentE.addAttribute("desc", desc); + + for (FileListEntry fle : files) { + XmlElement fileE = componentE.addChild("file"); + fileE.addAttribute("path", fle.getPath()); + fileE.addAttribute("size", fle.getSize()); + if (fle.getHash() != null) fileE.addAttribute("hash", fle.getHash()); + fileE.addAttribute("url", fle.getURL()); + } + + FileUtil.write(file, componentE.toXmlString()); + } + + public static Component fromXml(String baseFolder, XmlElement componentE) { + Component c = new Component(componentE.getAttribute("name"), componentE.getAttribute("desc")); + for (XmlElement fileE : componentE.getChildren("file")) { + try { + String fpath = fileE.getAttribute("path"); + long fsize = Long.parseLong(fileE.getAttribute("size")); + String fhash = fileE.getAttribute("hash"); + if (fhash.equals("")) fhash = null; + String furl = fileE.getAttribute("url"); + + String filePath = baseFolder+"/"+furl; + if (filePath.contains(".zip")) { + filePath = filePath.substring(0, filePath.indexOf(".zip")+4); + } + File file = new File(filePath); + + c.addFile(new FileListEntry(fpath, fsize, fhash, furl, file)); + } catch (NumberFormatException nfe) { + Log.w("Exception parsing installer component", nfe); + } + } + return c; + } +} diff --git a/UI/src/nl/weeaboo/vnds/installer/CreateCommand.java b/UI/src/nl/weeaboo/vnds/installer/CreateCommand.java new file mode 100644 index 0000000..9a8aaff --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/CreateCommand.java @@ -0,0 +1,47 @@ +package nl.weeaboo.vnds.installer; + +import java.io.File; +import java.io.IOException; + +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; + +public class CreateCommand extends PackCommand { + + private String baseFolder; + + public CreateCommand(String baseFolder, String outputFolder) { + super(outputFolder); + + this.baseFolder = baseFolder; + } + + //Functions + public void execute() { + File target = new File(getOutputFolder() + "/base_install"); + if (target.exists()) { + FileUtil.deleteFolder(target); + } + + target.mkdirs(); + try { + FileUtil.copyFolderContents(new File(baseFolder), target); + } catch (IOException e) { + Log.e("Exception executing create command", e); + } + + new File(target.getAbsolutePath()+"/background").mkdirs(); + new File(target.getAbsolutePath()+"/foreground").mkdirs(); + new File(target.getAbsolutePath()+"/script").mkdirs(); + new File(target.getAbsolutePath()+"/save").mkdirs(); + new File(target.getAbsolutePath()+"/sound").mkdirs(); + } + + //Getters + public String getBaseFolder() { + return baseFolder; + } + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/FileListEntry.java b/UI/src/nl/weeaboo/vnds/installer/FileListEntry.java new file mode 100644 index 0000000..e6d3919 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/FileListEntry.java @@ -0,0 +1,68 @@ +package nl.weeaboo.vnds.installer; + +import java.io.File; + +public class FileListEntry { + + private final String path; + private final long size; + private final String url; + private String hash; + + private File file; + + public FileListEntry(String path, long size, String hash, String url) { + this(path, size, hash, url, null); + } + public FileListEntry(String path, long size, String hash, String url, File file) { + this.path = path; + this.size = size; + this.hash = hash; + this.url = url; + + this.file = file; + } + + //Functions + public String toString() { + return String.format("%s %d %s %s", path, size, hash, url); + } + + public int hashCode() { + return path.hashCode(); + } + + public boolean equals(Object o) { + if (o instanceof FileListEntry) { + return equals((FileListEntry)o); + } + return false; + } + public boolean equals(FileListEntry entry) { + return path.equals(entry.getPath()) + && size == entry.getSize() + && ((getHash() == null && entry.getHash() == null) || getHash().equals(entry.getHash())); + //Don't compare URL's + } + + //Getters + public File getFile() { + return file; + } + + public String getPath() { + return path; + } + public long getSize() { + return size; + } + public String getHash() { + return hash; + } + public String getURL() { + return url; + } + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/Installer.java b/UI/src/nl/weeaboo/vnds/installer/Installer.java new file mode 100644 index 0000000..69529ea --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/Installer.java @@ -0,0 +1,182 @@ +package nl.weeaboo.vnds.installer; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.ProgressListener; +import nl.weeaboo.xml.XmlElement; +import nl.weeaboo.xml.XmlReader; + +public class Installer { + + //Functions + public static void main(String args[]) { + if (args.length < 2) { + System.err.println("Usage: java -jar Installer.jar "); + System.exit(1); + } + + String paths[] = Arrays.copyOfRange(args, 1, args.length); + install(args[0], null, paths); + } + + public static String install(String outputFolder, ProgressListener pl, String... componentPaths) { + StringBuilder sb = new StringBuilder(); + List files = new LinkedList(); + + int t = 0; + for (String path : componentPaths) { + if (pl != null) { + t++; + pl.onProgress(t, componentPaths.length, String.format("Reading %s...", path)); + } + + try { + File file = new File(path); + //String parentPath = file.getAbsoluteFile().getParent(); + + XmlReader xmlReader = new XmlReader(); + XmlElement componentE = xmlReader.read(file).getChild("component"); + Component component = Component.fromXml(new File(".").getAbsolutePath(), componentE); + + for (Iterator i = files.iterator(); i.hasNext(); ) { + FileListEntry f = i.next(); + for (FileListEntry nf : component.getFiles()) { + if (f.getPath().equals(nf.getPath())) { + i.remove(); //Overwrite old file + break; + } + } + } + + files.addAll(component.getFiles()); + } catch (Exception e) { + e.printStackTrace(); + sb.append(e); + sb.append('\n'); + } + } + + installFileList(outputFolder, pl, files); + + if (pl != null) pl.onFinished(""); + return sb.toString(); + } + + public static void installFileList(String outputFolder, ProgressListener pl, + Collection files) + { + FileUtil.deleteFolder(new File(outputFolder)); + + new File(outputFolder+"/foreground").mkdirs(); + new File(outputFolder+"/background").mkdirs(); + new File(outputFolder+"/script").mkdirs(); + new File(outputFolder+"/save").mkdirs(); + new File(outputFolder+"/sound").mkdirs(); + + byte readBuffer[] = new byte[10 * 1024 * 1024]; + Map zipFiles = new Hashtable(); + Map zipOutStreams = new Hashtable(); + + int t = 0; + for (FileListEntry entry : files) { + t++; + + try { + File file = entry.getFile(); + String path = entry.getPath(); + if (pl != null) { + pl.onProgress(t, files.size(), String.format("Installing %s...", path)); + } + + if (StringUtil.getExtension(file.getName()).equals("zip")) { + try { + ZipFile inZip = zipFiles.get(file); + if (inZip == null) { + zipFiles.put(file, inZip = new ZipFile(file)); + } + ZipOutputStream zout = zipOutStreams.get(file); + if (zout == null) { + String zipPath = outputFolder+"/"+path.substring(0, path.lastIndexOf(".zip")+4); + zipOutStreams.put(file, zout = new ZipOutputStream(new BufferedOutputStream( + new FileOutputStream(zipPath)))); + zout.setMethod(ZipOutputStream.STORED); + } + + String entryName = path.substring(path.indexOf(file.getName())+file.getName().length()+1); + + int fsize = 0; + { + //Read file data + ZipEntry zipEntry = inZip.getEntry(entryName); + if (zipEntry == null) continue; + InputStream in = new BufferedInputStream(inZip.getInputStream(zipEntry)); + + int r; + while ((r = in.read()) >= 0) { + readBuffer[fsize++] = (byte)r; + } + + in.close(); + } + { + //Write file data + + //Create ZIP Entry + ZipEntry zipEntry = new ZipEntry(entryName); + zipEntry.setSize(fsize); + zipEntry.setCompressedSize(fsize); + CRC32 crc = new CRC32(); + crc.update(readBuffer, 0, fsize); + zipEntry.setCrc(crc.getValue()); + zout.putNextEntry(zipEntry); + + //Write File contents to ZIP + zout.write(readBuffer, 0, fsize); + + zout.flush(); + zout.closeEntry(); + } + } catch (IOException ioe) { + Log.e("Exception during installation", ioe); + } + } else { + FileUtil.copyFile(file, new File(outputFolder+"/"+path)); + } + } catch (Exception e) { + Log.e("Exception during installation", e); + } + } + + //Close open files + for (ZipFile file : zipFiles.values()) { + try { file.close(); } catch (IOException e) {} + } + for (ZipOutputStream zout : zipOutStreams.values()) { + try { zout.close(); } catch (IOException e) {} + } + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/InstallerPacker.java b/UI/src/nl/weeaboo/vnds/installer/InstallerPacker.java new file mode 100644 index 0000000..718b245 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/InstallerPacker.java @@ -0,0 +1,228 @@ +package nl.weeaboo.vnds.installer; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.vnds.HashUtil; +import nl.weeaboo.vnds.Log; + +public class InstallerPacker { + + //Functions + private static void printUsage() { + System.err.println("Usage: java -jar InstallPacker.jar \n" + + "\t\tcommands:\n" + + "\tcreate \n" + + "\tpatch \n" + + "\tadd \n" + + "\n"); + } + + public static PackCommand parseCommand(String args[]) { + if (args[0].equals("create")) { + return new CreateCommand(args[1], args[2]); + } else if (args[0].equals("patch")) { + return new PatchCommand(args[1], args[2], args[3]); + } else if (args[0].equals("add")) { + return new AddCommand(args[1], args[2]); + } + Log.w("Unknown Command: " + args[0]); + return null; + } + + public static void main(String args[]) { + if (args.length < 1) { + printUsage(); + return; + } + + PackCommand c = parseCommand(args); + if (c == null) { + printUsage(); + return; + } + + c.execute(); + } + + public static void execute(String command) { + List args = new ArrayList(); + + String parts[] = command.split(" "); + + boolean inString = false; + StringBuilder buffer = new StringBuilder(); + for (int n = 0; n < parts.length; n++) { + parts[n] = parts[n].trim(); + + if (!inString && parts[n].startsWith("\"")) { + inString = true; + parts[n] = parts[n].substring(1); + } + + if (inString) { + if (buffer.length() > 0) buffer.append(' '); + buffer.append(parts[n]); + } else { + args.add(parts[n]); + } + + if (inString && parts[n].endsWith("\"")) { + args.add(buffer.substring(0, buffer.length()-1)); //Don't include trailing (") + buffer.delete(0, buffer.length()); + inString = false; + } + } + + parseCommand(args.toArray(new String[args.size()])).execute(); + } + + public static void collectFiles(Map map, File file, boolean includeRootFolder) { + if (!file.isDirectory() && !StringUtil.getExtension(file.getName()).equals("zip")) { + map.put(file.getName(), file); + } else { + if (includeRootFolder) { + collectFiles(map, file, ""); + } else { + if (file.isDirectory()) { + for (File f : file.listFiles()) { + collectFiles(map, f, ""); + } + } else if (StringUtil.getExtension(file.getName()).equals("zip")) { + collectZippedFiles(map, file, ""); + } + } + } + } + public static void collectZippedFiles(Map map, File file, String relPath) { + try { + ZipFile zipFile = new ZipFile(file); + + Enumeration enumeration = zipFile.entries(); + while (enumeration.hasMoreElements()) { + ZipEntry entry = enumeration.nextElement(); + map.put(relPath + '/' + entry.getName(), file); + } + + zipFile.close(); + } catch (ZipException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + private static void collectFiles(Map map, File file, String relPath) { + String path; + if (relPath.length() > 0) { + path = relPath + '/' + file.getName(); + } else { + path = file.getName(); + } + + if (file.isDirectory()) { + for (File f : file.listFiles()) { + collectFiles(map, f, path); + } + } else if (StringUtil.getExtension(file.getName()).equals("zip")) { + collectZippedFiles(map, file, path); + } else { + map.put(path, file); + } + } + + public static Set generateFileList(Map files, String baseURL, boolean skipHash) { + byte readBuffer[] = new byte[10 * 1024 * 1024]; + + Map zipFiles = new Hashtable(); + Set entries = new HashSet(); + + int t = 0; + for (Entry entry : files.entrySet()) { + t++; + //System.out.printf("(%d/%d) %s\n", t, files.size(), entry.getKey()); + if ((t & 0xFF) == 0) { + System.out.printf("Files Hashed: %d/%d\n", t, files.size()); + } + + File file = entry.getValue(); + if (!file.exists()) { + continue; + } + + try { + String fpath = entry.getKey(); + long fsize = file.length(); + String fhash = null; + if (StringUtil.getExtension(file.getName()).equals("zip")) { + try { + if (!zipFiles.containsKey(file)) { + zipFiles.put(file, new ZipFile(file)); + } + ZipFile zip = zipFiles.get(file); + ZipEntry zipEntry = zip.getEntry(fpath.substring(fpath.indexOf(file.getName()) + + file.getName().length() + 1)); + + InputStream in = new BufferedInputStream(zip.getInputStream(zipEntry)); + if (!skipHash) { + fsize = readFromStream(readBuffer, in); + fhash = HashUtil.hashToString(HashUtil.generateHash(readBuffer, 0, (int)fsize)); + } + in.close(); + } catch (IOException ioe) { + Log.e("Error hashing file", ioe); + } + } + if (fhash == null) { + if (!skipHash) { + BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); + fsize = readFromStream(readBuffer, in); + fhash = HashUtil.hashToString(HashUtil.generateHash(readBuffer, 0, (int)fsize)); + in.close(); + } + } + String furl = baseURL + "/" + fpath; + + entries.add(new FileListEntry(fpath, fsize, fhash, furl, file)); + } catch (Exception e) { + Log.e("Exception generating file list", e); + } + } + + for (ZipFile file : zipFiles.values()) { + try { + file.close(); + } catch (IOException e) {} + } + return entries; + } + + protected static int readFromStream(byte[] out, InputStream in) throws IOException { + int off = 0; + while (off < out.length) { + int r = in.read(out, off, out.length-off); + if (r < 0) break; + off += r; + } + return off; + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/PackCommand.java b/UI/src/nl/weeaboo/vnds/installer/PackCommand.java new file mode 100644 index 0000000..8ab62f2 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/PackCommand.java @@ -0,0 +1,21 @@ +package nl.weeaboo.vnds.installer; + +abstract class PackCommand { + + private String outputFolder; + + public PackCommand(String outputFolder) { + this.outputFolder = outputFolder; + } + + //Functions + public abstract void execute(); + + //Getters + public String getOutputFolder() { + return outputFolder; + } + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/installer/PatchCommand.java b/UI/src/nl/weeaboo/vnds/installer/PatchCommand.java new file mode 100644 index 0000000..57cf7ab --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/installer/PatchCommand.java @@ -0,0 +1,73 @@ +package nl.weeaboo.vnds.installer; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.vnds.Log; + +public class PatchCommand extends PackCommand { + + private String patchName; + private String patchedFolder; + + public PatchCommand(String patchName, String patchedFolder, String outputFolder) { + super(outputFolder); + + this.patchName = patchName; + this.patchedFolder = patchedFolder; + } + + //Functions + public void execute() { + boolean skipHash = false; + String baseURL = "patch/" + patchName; + + Map patchedFiles = new Hashtable(); + InstallerPacker.collectFiles(patchedFiles, new File(patchedFolder), false); + Set patchedEntries = InstallerPacker.generateFileList(patchedFiles, baseURL, skipHash); + + Map baseFiles = new Hashtable(); + InstallerPacker.collectFiles(baseFiles, new File(getOutputFolder() + "/base_install"), false); + Set baseEntries = InstallerPacker.generateFileList(baseFiles, baseURL, skipHash); + + List changedList = new ArrayList(); + for (FileListEntry entry : patchedEntries) { + if (!baseEntries.contains(entry)) { + changedList.add(entry); + } + } + + try { + Component component = new Component(patchName, ""); + String patchFolder = getOutputFolder() + "/" + patchName; + for (FileListEntry changed : changedList) { + component.addFile(changed); + + File target = new File(patchFolder + '/' + changed.getPath()); + if (StringUtil.getExtension(changed.getFile().getName()).equals("zip")) { + target = new File(patchFolder + '/' + changed.getPath().substring(0, + changed.getPath().lastIndexOf(changed.getFile().getName()) + +changed.getFile().getName().length())); + } + if (!target.exists() || target.length() != changed.getFile().length()) { + FileUtil.copyFile(changed.getFile(), target); + } + } + + component.save(new File(getOutputFolder(), patchName + ".xml")); + } catch (IOException e) { + Log.e("Exception executing patch command", e); + } + } + + //Getters + + //Setters +} diff --git a/UI/src/nl/weeaboo/vnds/tools/ImageConverter.java b/UI/src/nl/weeaboo/vnds/tools/ImageConverter.java new file mode 100644 index 0000000..46b0d1b --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/ImageConverter.java @@ -0,0 +1,543 @@ +package nl.weeaboo.vnds.tools; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; + +import javax.imageio.ImageIO; + +import nl.weeaboo.awt.ImageUtil; +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.system.ProcessUtil; +import nl.weeaboo.tga.TGAUtil; +import nl.weeaboo.vnds.BatchProcess; +import nl.weeaboo.vnds.FileOp; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.ProgressListener; +import nl.weeaboo.vnds.VNImageUtil; + +public class ImageConverter { + + public enum ConvertType { + TYPE_RAW_RGBA("RAW RGBA", "dta"), + TYPE_RAW_RGB256("RAW RGB256", "dta"), + TYPE_JPG("JPG", "jpg"), + TYPE_PNG("PNG", "png"), + TYPE_PNG_256_NEUQUANT("PNG 256 (neuquant)", "png"), + TYPE_PNG_256_MEDIAN("PNG 256 (median)", "png"); + + private String label; + private String fileExt; + + private ConvertType(String label, String fileExt) { + this.label = label; + this.fileExt = fileExt; + } + + public String getFileExt() { return fileExt; } + public String toString() { return label; } + } + + public enum ScalingType { + NONE("None"), BACKGROUND("Background"), SPRITE("Sprite"), STRETCH("Stretch"); + + String label; + + private ScalingType(String label) { + this.label = label; + } + + public String toString() { return label; } + } + + public enum DitheringType { + NONE("None"), RANDOM("Random"), FLOYD_STEINBERG("Floyd-Steinberg"); + + String label; + + private DitheringType(String label) { + this.label = label; + } + + public String toString() { return label; } + } + + private int maxThreads = 8; + private ConvertType mode = ConvertType.TYPE_RAW_RGBA; + private ScalingType scaling = ScalingType.NONE; + private Dimension targetScreenSize = new Dimension(256, 192); + private Dimension srcScreenSize = new Dimension(800, 600); //Matters for sprites only + private int quality = 98; //JPG Only + private DitheringType dithering = DitheringType.NONE; + private boolean log; + + //Temporary vars + private Map processLogs; + + //Functions + protected static void printUsage() { + System.err.printf("Usage: java -jar ImageConverter.jar \nflags:" + + "\n\t-threads " + + "\n\t-raw " + + "\n\t-png " + + "\n\t-jpg " +// + "\n\t-size " + + "\n"); + } + + public static void main(String args[]) throws IOException { + ImageConverter ic = new ImageConverter(); + + String filename = null; + try { + for (int n = 0; n < args.length; n++) { + if (args[n].startsWith("-jpg")) { + ic.setMode(ConvertType.TYPE_JPG); + ic.quality = Integer.parseInt(args[++n]); + } else if (args[n].startsWith("-png")) { + if (!args[++n].equals("256")) { + ic.setMode(ConvertType.TYPE_PNG); + } else { + ic.setDitheringType(DitheringType.FLOYD_STEINBERG); + ic.setMode(ConvertType.TYPE_PNG_256_NEUQUANT); + } + } else if (args[n].startsWith("-raw")) { + if (!args[++n].equals("256")) { + ic.setMode(ConvertType.TYPE_RAW_RGBA); + } else { + ic.setDitheringType(DitheringType.FLOYD_STEINBERG); + ic.setMode(ConvertType.TYPE_RAW_RGB256); + } + } else if (args[n].startsWith("-threads")) { + ic.maxThreads = Integer.parseInt(args[++n]); +/* + } else if (args[n].startsWith("-size")) { + int w = Integer.parseInt(args[++n]); + int h = Integer.parseInt(args[++n]); + ic.setBackgroundSize(w, h); +*/ + } else if (filename == null) { + filename = args[n]; + } else { + throw new IllegalArgumentException("Error parsing arg: " + args[n]); + } + } + } catch (RuntimeException re) { + printUsage(); + return; + } + + if (filename == null) { + printUsage(); + return; + } + + ic.convertFolder(filename, new ProgressListener() { + public void onFinished(String message) { + System.out.printf("%s\n", message); + } + public void onProgress(int value, int max, String message) { + System.out.printf("Processing (%d/%d) %s...\n", value, max, message); + } + }); + } + + public void convertFolder(String folder, final ProgressListener pl) throws IOException { + convertFolder(folder, pl, 1); + } + public void convertFolder(String folder, final ProgressListener pl, int maxDepth) throws IOException { + processLogs = new HashMap(); + + Map files = new HashMap(); + File folderF = new File(folder).getCanonicalFile(); + if (folderF.isDirectory()) { + for (File file : folderF.listFiles()) { + if (!file.isDirectory()) files.put(file.getName(), file); + } + } else { + files.put(folderF.getName(), folderF); + } + + BatchProcess bp = new BatchProcess(); + bp.setTaskSize(32); + bp.setThreads(maxThreads); + bp.setThreadPriority(Thread.MIN_PRIORITY); + bp.addProgressListener(pl); + try { + bp.run(files, new FileOp() { + @Override + public void execute(String relpath, File file) throws IOException { + convertFile(file); + } + }); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public File convertFile(File file) { + return convertFile(file, null); + } + public File convertFile(File file, File targetFolder) { + String filenameNoExt = file.getName(); + if (filenameNoExt.lastIndexOf('.') > 0) { + filenameNoExt = filenameNoExt.substring(0, filenameNoExt.lastIndexOf('.')); + } + + StringBuilder log = new StringBuilder(); + if (isLogging()) processLogs.put(file, log); + + try { + BufferedImage result = null; + try { + if (file.getName().endsWith(".bmp")) { + result = VNImageUtil.readBMP(file); + } else if (file.getName().endsWith(".tga")) { + result = TGAUtil.readTGA(file); + } else { + result = ImageIO.read(file); + } + } catch (RuntimeException re) { + Log.w("Exception while reading image", re); + } + if (result == null) { + Log.e("Unreadable image: " + file.getAbsolutePath()); + return null; + } + + int w = result.getWidth(); + int h = result.getHeight(); + + //Scaling + if (scaling == ScalingType.STRETCH) { + w = targetScreenSize.width; + h = targetScreenSize.height; + result = ImageUtil.getScaledImage(result, w, h, Image.SCALE_AREA_AVERAGING); + } else { + int scaledW = w; + int scaledH = h; + float scale = Math.min(targetScreenSize.width / (float)srcScreenSize.width, + targetScreenSize.height / (float)srcScreenSize.height); + if (scaling == ScalingType.SPRITE || scaling == ScalingType.BACKGROUND) { + scaledW = Math.max(1, Math.round(w * scale)); + scaledH = Math.max(1, Math.round(h * scale)); + } + + Image scaled = ImageUtil.getScaledImage(result, scaledW, scaledH, Image.SCALE_AREA_AVERAGING); + + if (scaling == ScalingType.BACKGROUND) { + w = targetScreenSize.width; + h = targetScreenSize.height; + } else { + w = scaledW; + h = scaledH; + } + + result = ImageUtil.createCompatibleImage(w, h, scaled); + if (mode == ConvertType.TYPE_JPG) { + result = ImageUtil.asOpaqueImage(result); + } + + Graphics2D g = (Graphics2D)result.getGraphics(); + if (scaling == ScalingType.BACKGROUND) { + int sw = Math.round(scale*srcScreenSize.width); + int sh = Math.round(scale*srcScreenSize.height); + g.clipRect((w-sw)/2, (h-sh)/2, sw, sh); + } + g.drawImage(scaled, (w-scaledW)/2, (h-scaledH)/2, null); + g.dispose(); + } + + //Dithering + if (dithering == DitheringType.RANDOM) { + int[] rgb = result.getRGB(0, 0, w, h, null, 0, w); + + Random rnd = new Random(0x13371337); + int t = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int c = rgb[t]; + + double r = ((c>>16)&0xFF) * 31.0 / 255.0; + double g = ((c>>8 )&0xFF) * 31.0 / 255.0; + double b = ((c )&0xFF) * 31.0 / 255.0; + + boolean ceil = rnd.nextFloat() < (r-Math.floor(r) + g-Math.floor(g) + b-Math.floor(b)) / 3.0; + + int ri = (int)(ceil ? Math.ceil(r) : Math.floor(r)); + int gi = (int)(ceil ? Math.ceil(g) : Math.floor(g)); + int bi = (int)(ceil ? Math.ceil(b) : Math.floor(b)); + + rgb[t] = (c&0xFF000000) | (((ri<<3)&0xFF)<<16) | (((gi<<3)&0xFF)<<8) | ((bi<<3)&0xFF); + t++; + } + } + + result.setRGB(0, 0, w, h, rgb, 0, w); + } else if (dithering == DitheringType.FLOYD_STEINBERG) { + int[] rgb = result.getRGB(0, 0, w, h, null, 0, w); + floydSteinberg(rgb, w, h); + result.setRGB(0, 0, w, h, rgb, 0, w); + } + + if (targetFolder == null) { + targetFolder = file.getParentFile(); + file.delete(); + } + + //Create unique hash (multiple threads are writing temp files in the same folder) + String threadHash = String.valueOf(hashCode() ^ file.hashCode() ^ Thread.currentThread().hashCode()); + + file = new File(String.format("%s/%s.%s", + targetFolder, filenameNoExt, mode.getFileExt().toLowerCase())); + if (file.getParentFile() != null) { + file.getParentFile().mkdirs(); + } + + if (mode.getFileExt().equalsIgnoreCase("jpg")) { + String bmpFile = String.format("%s/__%s.bmp", targetFolder, threadHash); + String tmpFile = String.format("%s/__%s.jpg", targetFolder, threadHash); + + ImageIO.write(result, "bmp", new File(bmpFile)); + + Process p = ProcessUtil.execInDir(String.format( + "cjpeg -quality %d -optimize -dct fast \"%s\" \"%s\"", + quality, bmpFile, tmpFile), + "tools/"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + file.delete(); + new File(bmpFile).delete(); + new File(tmpFile).renameTo(file); + } else if (mode.getFileExt().equalsIgnoreCase("png")) { + String tmpFile = String.format("%s/__%s.png", targetFolder, threadHash); + ImageIO.write(result, "png", new File(tmpFile)); + + if (mode == ConvertType.TYPE_PNG_256_MEDIAN) { + Process p = ProcessUtil.execInDir(String.format( + "pngquant 256 \"%s\"", tmpFile), + "tools/pngquant-0.95/"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + file.delete(); + new File(StringUtil.stripExtension(tmpFile)+"-fs8.png").renameTo(file); + } else if (mode == ConvertType.TYPE_PNG_256_NEUQUANT) { + Process p = ProcessUtil.execInDir(String.format( + "pngnq \"%s\"", tmpFile), + "tools/pngnq-0.5-i386/"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + file.delete(); + new File(StringUtil.stripExtension(tmpFile)+"-nq8.png").renameTo(file); + } else { + String crushedName = StringUtil.stripExtension(tmpFile)+".crushed.png"; + Process p = ProcessUtil.execInDir(String.format( + "pngcrush -fix \"%s\" \"%s\"", tmpFile, crushedName), + "tools/pngcrush-1.6.10/"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + file.delete(); + new File(crushedName).renameTo(file); + } + + new File(tmpFile).delete(); + } else if (mode.getFileExt().equalsIgnoreCase("dta")) { + if (mode == ConvertType.TYPE_RAW_RGBA) { + int[] rgb = result.getRGB(0, 0, w, h, null, 0, w); + + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(file)); + for (int c : rgb) { + int a = ((c>>>24) >= 127 ? (1<<15) : 0); + int r = (c>>19) & 31; + int g = (c>>11) & 31; + int b = (c>>3) & 31; + c = a | (b<<10) | (g<<5) | (r); + + bout.write(c&0xFF); + bout.write((c>>8)&0xFF); + } + bout.flush(); + bout.close(); + } else if (mode == ConvertType.TYPE_RAW_RGB256) { + File tmpFile = new File(String.format("%s/__%s.png", targetFolder, threadHash)); + ImageIO.write(result, "png", tmpFile); + + Process p = ProcessUtil.execInDir(String.format( + "pngnq \"%s\"", tmpFile.getAbsolutePath()), + "tools/pngnq-0.5-i386/"); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + tmpFile.delete(); + new File(StringUtil.stripExtension(tmpFile.getAbsolutePath())+"-nq8.png").renameTo(tmpFile); + + result = ImageIO.read(tmpFile); + IndexColorModel icm = (IndexColorModel)result.getColorModel(); + + BufferedOutputStream bout; + + bout = new BufferedOutputStream(new FileOutputStream( + StringUtil.stripExtension(file.getAbsolutePath())+".pal")); + for (int n = 0; n < icm.getMapSize(); n++) { + int a = (1<<15); + int r = icm.getRed(n) >> 3; + int g = icm.getGreen(n) >> 3; + int b = icm.getBlue(n) >> 3; + int c = a | (b<<10) | (g<<5) | (r); + + bout.write(c&0xFF); + bout.write((c>>8)&0xFF); + } + bout.close(); + + bout = new BufferedOutputStream(new FileOutputStream(file)); + int dta[] = new int[result.getWidth() * result.getHeight()]; + result.getRaster().getPixels(0, 0, result.getWidth(), result.getHeight(), dta); + for (int n = 0; n < dta.length; n++) { + bout.write(dta[n]); + } + bout.close(); + + tmpFile.delete(); + } + } else { + throw new IllegalArgumentException("Invalid file-ext: " + mode.getFileExt()); + } + return file; + } catch (Exception e) { + Log.w(file.getName() + " " + e); + log.append(e.toString()); + } + + return null; + } + + + public File dumpLog(String filename) throws IOException { + File file = new File(filename); + PrintWriter out = new PrintWriter(new FileWriter(file)); + + out.println("----------------------------------------"); + out.println("----------------------------------------"); + for (Entry entry : getLogs().entrySet()) { + out.println(); + out.println("Log for file:" + entry.getKey().getAbsolutePath()); + out.println(); + + out.println(entry.getValue().toString()); + + out.println("----------------------------------------"); + out.println("----------------------------------------"); + } + + out.close(); + + return file; + } + + private static int round(float f) { + return (int)f; + } + + private static float saturate255(float f) { + if (f < 0f) return 0f; + if (f > 255f) return 255f; + return f; + } + + /** Does dithering for 5-bit colors (equivalent to 8-bit / 8.0) */ + private static void floydSteinberg(int rgb[], int w, int h) { + int L = w * h; + + float rgba[][] = new float[4][L]; + for (int t = 0; t < L; t++) { + rgba[0][t] = ((rgb[t]>>>16)&0xFF); + rgba[1][t] = ((rgb[t]>>>8 )&0xFF); + rgba[2][t] = ((rgb[t] )&0xFF); + rgba[3][t] = ((rgb[t]>>>24)&0xFF); + } + + final float DIV_8 = .125f; + final float DIV_16 = .0625f; + final int RIGHT = 1; + final int DOWN = w; + final int DOWN_LEFT = DOWN-1; + final int DOWN_RIGHT = DOWN+1; + + for (int c = 0; c < 3; c++) { + float[] p = rgba[c]; + int t = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + float oldc = p[t]; + float newc = p[t] = round(oldc * DIV_8) << 3; + float error = oldc - newc; + + if (x+1 < w) { + p[t+RIGHT] += (7 * error) * DIV_16; + p[t+RIGHT] = saturate255(p[t+RIGHT]); + } + if (y+1 < h) { + if (x-1 >= 0) { + p[t+DOWN_LEFT] += (3 * error) * DIV_16; + p[t+DOWN_LEFT] = saturate255(p[t+DOWN_LEFT]); + } + p[t+DOWN] += (5 * error) * DIV_16; + p[t+DOWN] = saturate255(p[t+DOWN]); + if (x+1 < w) { + p[t+DOWN_RIGHT] += error * DIV_16; + p[t+DOWN_RIGHT] = saturate255(p[t+DOWN_RIGHT]); + } + } + + t++; + } + } + } + + int t = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int a = Math.min(255, Math.max(0, (int)Math.round(rgba[3][t]))); + int r = Math.min(255, Math.max(0, (int)Math.round(rgba[0][t]))); + int g = Math.min(255, Math.max(0, (int)Math.round(rgba[1][t]))); + int b = Math.min(255, Math.max(0, (int)Math.round(rgba[2][t]))); + rgb[t] = (a<<24)|(r<<16)|(g<<8)|(b); + t++; + } + } + } + + // Getters + public Map getLogs() { return processLogs; } + public boolean isLogging() { return log; } + public ConvertType getMode() { return mode; } + public ScalingType getScaling() { return scaling; } + public Dimension getTargetScreenSize() { return new Dimension(targetScreenSize); } + public Dimension getSourceScreenSize() { return new Dimension(srcScreenSize); } + public int getQuality() { return quality; } + public DitheringType getDitheringType() { return dithering; } + public int getMaxThreads() { return maxThreads; } + + // Setters + public void setMode(ConvertType mode) { this.mode = mode; } + public void setScalingType(ScalingType s) { this.scaling = s; } + public void setLogging(boolean l) { this.log = l; } + public void setSourceScreenSize(int w, int h) { srcScreenSize.width = w; srcScreenSize.height = h; } + public void setTargetScreenSize(int w, int h) { targetScreenSize.width = w; targetScreenSize.height = h; } + public void setQuality(int quality) { this.quality = quality; } + public void setDitheringType(DitheringType dithering) { this.dithering = dithering; } + public void setMaxThreads(int mt) { this.maxThreads = mt; } + +} diff --git a/UI/src/nl/weeaboo/vnds/tools/ImageConverterGUI.java b/UI/src/nl/weeaboo/vnds/tools/ImageConverterGUI.java new file mode 100644 index 0000000..c7fe43d --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/ImageConverterGUI.java @@ -0,0 +1,328 @@ +package nl.weeaboo.vnds.tools; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerNumberModel; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.awt.AwtUtil; +import nl.weeaboo.awt.DirectValidatingField; +import nl.weeaboo.awt.FileBrowseField; +import nl.weeaboo.awt.Sash; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.ProgressListener; +import nl.weeaboo.vnds.ProgressRunnable; +import nl.weeaboo.vnds.VNDSProgressDialog; +import nl.weeaboo.vnds.tools.ImageConverter.ConvertType; +import nl.weeaboo.vnds.tools.ImageConverter.DitheringType; +import nl.weeaboo.vnds.tools.ImageConverter.ScalingType; + +/* + * Changes: + * + * 2009/11/08 -- v1.0.5 + * - Code cleanup + * - Converts input files even if they're already the correct file type + * + * 2009/05/01 -- v1.0.4 + * - Added RAW_RGB256 mode + * + * 2009/02/28 -- v1.0.3 + * - Using external programs to create better compressed images that also + * actually work on vnds. (cjpeg, pngquant, pngnq, pngcrush) + * + * 2009/02/17 -- v1.0.2 + * - Fixed bug in Floyd-Steinberg dithering + * - Maybe fixed JPG's not working in vnds + * + * 2008/12/20 -- v1.0.1 + * - Drag images into the window to convert + * + * 2008/11/25 -- v1.0 + * - Initial Release + * + */ +@SuppressWarnings("serial") +public class ImageConverterGUI extends JFrame { + + private ImageConverter converter; + + private JComboBox modeCombo; + private JComboBox scalingCombo; + private JTextField widthField; + private JTextField heightField; + private JSpinner qualitySpinner; + private JComboBox ditheringCombo; + private JCheckBox loggingCheck; + private JSpinner threadsSpinner; + private FileBrowseField browseField; + private JButton convertButton; + + public ImageConverterGUI() { + converter = new ImageConverter(); + + setTitle("VNDS Image Converter v1.0.5"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + setLayout(new BorderLayout()); + add(createCenterPanel(), BorderLayout.CENTER); + + reset(); + + setMinimumSize(new Dimension(300, 100)); + pack(); + setLocationRelativeTo(null); + setVisible(true); + } + + //Functions + public static void main(String args[]) { + AwtUtil.setDefaultLAF(); + + if (args.length > 0) { + try { + ImageConverter.main(args); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + new ImageConverterGUI(); + } + } + + protected JPanel createCenterPanel() { + JLabel modeLabel = new JLabel("Output Type"); + modeCombo = new JComboBox(ImageConverter.ConvertType.values()); + modeCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + ConvertType mode = (ConvertType)modeCombo.getSelectedItem(); + converter.setMode(mode); + qualitySpinner.setEnabled(mode == ConvertType.TYPE_JPG); + } + }); + + JLabel scalingLabel = new JLabel("Scaling Mode"); + scalingCombo = new JComboBox(ImageConverter.ScalingType.values()); + scalingCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + ScalingType s = (ScalingType)scalingCombo.getSelectedItem(); + converter.setScalingType(s); + } + }); + + JLabel sizeLabel = new JLabel("Scaling Mode Size"); + widthField = new DirectValidatingField() { + public boolean isValid(String text) { + try { + int i = Integer.parseInt(text); + return (i > 0 && i <= 2048); + } catch (NumberFormatException nfe) { + } + return false; + } + protected void onValidTextEntered(String text) { + int w = Integer.parseInt(text); + converter.setSourceScreenSize(w, converter.getSourceScreenSize().height); + } + }; + heightField = new DirectValidatingField() { + public boolean isValid(String text) { + try { + int i = Integer.parseInt(text); + return (i > 0 && i <= 2048); + } catch (NumberFormatException nfe) { + } + return false; + } + protected void onValidTextEntered(String text) { + int h = Integer.parseInt(text); + converter.setSourceScreenSize(converter.getSourceScreenSize().width, h); + } + }; + JLabel qualityLabel = new JLabel("JPEG Quality"); + qualitySpinner = new JSpinner(new SpinnerNumberModel(98, 0, 100, 1)); + qualitySpinner.setEnabled(false); + + JLabel ditheringLabel = new JLabel("Dithering"); + ditheringCombo = new JComboBox(new Object[] {DitheringType.NONE, + DitheringType.FLOYD_STEINBERG, DitheringType.RANDOM}); + ditheringCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + DitheringType dt = (DitheringType)ditheringCombo.getSelectedItem(); + converter.setDitheringType(dt); + } + }); + + JLabel loggingLabel = new JLabel("Write logfile"); + loggingCheck = new JCheckBox(); + JLabel threadsLabel = new JLabel("Threads"); + threadsSpinner = new JSpinner(new SpinnerNumberModel(8, 1, 128, 1)); + + JPanel sizePanel = new JPanel(new GridLayout(-1, 3, 5, 5)); + sizePanel.add(widthField); + sizePanel.add(new JLabel(" x ", JLabel.CENTER)); + sizePanel.add(heightField); + + JPanel panel = new JPanel(new GridLayout(-1, 2, 15, 5)); + panel.add(modeLabel); panel.add(modeCombo); + panel.add(scalingLabel); panel.add(scalingCombo); + panel.add(sizeLabel); panel.add(sizePanel); + panel.add(qualityLabel); panel.add(qualitySpinner); + panel.add(ditheringLabel); panel.add(ditheringCombo); + panel.add(loggingLabel); panel.add(loggingCheck); + panel.add(threadsLabel); panel.add(threadsSpinner); + + browseField = FileBrowseField.readFolder("Folder", new File("")); + convertButton = new JButton("Convert"); + convertButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + convert(); + } + }); + + JPanel bottomPanel = new JPanel(new BorderLayout(10, 10)); + bottomPanel.add(new Sash(Sash.HORIZONTAL), BorderLayout.NORTH); + bottomPanel.add(browseField, BorderLayout.CENTER); + bottomPanel.add(convertButton, BorderLayout.EAST); + + JPanel panel2 = new JPanel(new BorderLayout(0, 10)); + panel2.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel2.add(panel, BorderLayout.NORTH); + panel2.add(bottomPanel, BorderLayout.SOUTH); + + new DropTarget(panel2, new DropTargetListener() { + public void dragEnter(DropTargetDragEvent dtde) { + } + public void dragExit(DropTargetEvent dte) { + } + public void dragOver(DropTargetDragEvent dtde) { + for (DataFlavor df : dtde.getCurrentDataFlavorsAsList()) { + if (df.isFlavorJavaFileListType()) { + dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE); + break; + } + } + } + @SuppressWarnings("unchecked") + public void drop(DropTargetDropEvent dtde) { + dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); + + Transferable t = dtde.getTransferable(); + for (DataFlavor df : t.getTransferDataFlavors()) { + if (df.isFlavorJavaFileListType()) { + try { + List list = (List)t.getTransferData(df); + convert(list); + } catch (UnsupportedFlavorException e) { + Log.w("Drag&Drop Exception", e); + } catch (IOException e) { + Log.w("Drag&Drop Exception", e); + } + } + } + } + public void dropActionChanged(DropTargetDragEvent dtde) { + } + }); + + return panel2; + } + + public void convert() { + convert(null); + } + public void convert(final List files) { + final File folder = browseField.getFile(); + if (files == null) { + if (!folder.exists() || !folder.isDirectory()) { + JOptionPane.showMessageDialog(null, "Invalid directory: \"" + folder + "\"", + "Error", JOptionPane.ERROR_MESSAGE); + return; + } + } + + converter.setMode((ConvertType)modeCombo.getSelectedItem()); + converter.setScalingType((ScalingType)scalingCombo.getSelectedItem()); + converter.setQuality((Integer)qualitySpinner.getValue()); + converter.setLogging(loggingCheck.isSelected()); + converter.setMaxThreads((Integer)threadsSpinner.getValue()); + + ProgressListener pl = new ProgressListener() { + public void onFinished(String message) { + String logPath = ""; + String logPart = ""; + + try { + if (converter.isLogging()) { + logPath = converter.dumpLog("conversion.log").getAbsolutePath(); + logPart = String.format("

Log dumped to %s", logPath); + } + + JOptionPane.showMessageDialog(null, String.format( + "Conversion finished.%s", logPart), + "Finished", JOptionPane.PLAIN_MESSAGE); + } catch (IOException e) { + AwtUtil.showError(e); + } + } + public void onProgress(int value, int max, String message) { + } + }; + + ProgressRunnable task = new ProgressRunnable() { + public void run(ProgressListener pl) { + if (files == null) { + try { + converter.convertFolder(folder.toString(), pl); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + for (File f : files) { + converter.convertFile(f); + } + } + } + }; + + VNDSProgressDialog dialog = new VNDSProgressDialog(); + dialog.showDialog(task, pl); + } + + public void reset() { + widthField.setText("800"); + heightField.setText("600"); + qualitySpinner.setValue(converter.getQuality()); + threadsSpinner.setValue(converter.getMaxThreads()); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/tools/MaskConverter.java b/UI/src/nl/weeaboo/vnds/tools/MaskConverter.java new file mode 100644 index 0000000..8139c09 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/MaskConverter.java @@ -0,0 +1,76 @@ +package nl.weeaboo.vnds.tools; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +public class MaskConverter { + + //Functions + public static void main(String args[]) throws IOException { + if (args.length < 1) { + System.err.println("Usage: MaskConverter "); + return; + } + + process(new File(args[0])); + } + + private static void process(File file) { + if (file.isDirectory()) { + for (File f : file.listFiles()) { + if (f.getName().toLowerCase().endsWith("jpg") + || f.getName().toLowerCase().endsWith("bmp")) + { + process(f); + } + } + } else { + try { + BufferedImage image = ImageIO.read(file); + int w = image.getWidth(); + int w2 = w / 2; + int h = image.getHeight(); + + int rgb[] = image.getRGB(0, 0, w, h, new int[w * h], 0, w); + for (int y = 0; y < h; y++) { + int offset = y * w; + for (int x = 0; x < w2; x++) { + int c = rgb[offset]; + int alpha = rgb[offset + w2]; + alpha = 255 - (int)(0.30f * ((alpha >> 16) & 0xFF) + + 0.59f * ((alpha >> 8) & 0xFF) + + 0.11f * (alpha & 0xFF)); + alpha = Math.max(0, Math.min(255, alpha)); + + rgb[offset] = (alpha<<24) | (c & 0xFFFFFF); + offset++; + } + } + + //Write back to an image + BufferedImage result = new BufferedImage(w2, h, BufferedImage.TYPE_INT_ARGB); + result.setRGB(0, 0, w2, h, rgb, 0, w); + + //Write to disc + String path = file.getAbsolutePath(); + path = path.substring(0, path.length() - 3) + "png"; + ImageIO.write(result, "png", new File(path)); + + //Delete original + file.delete(); + + System.out.println("Writing: " + path); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/tools/Octree.java b/UI/src/nl/weeaboo/vnds/tools/Octree.java new file mode 100644 index 0000000..78da7e6 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/Octree.java @@ -0,0 +1,293 @@ +package nl.weeaboo.vnds.tools; + +/** + * Octree.java ----------- + * + * @author Sascha L. Teichmann (sascha@intevation.de) + * @version 0.1 date 21:31:02 Mi 07-Mrz-2001 + */ + +public class Octree { + + static public class Node { + Node parent; + Node children[] = new Node[8]; + + int midRed, midGreen, midBlue; + + int totalRed, totalGreen, totalBlue; + + byte id; // which child of parent? + byte census; // used children + byte level; // level in tree + + int colorNumber; // index in color table + + int numberUnique; + int numberColors; + + public Node() { + } + + public Node(int midRed, int midGreen, int midBlue, Node parent, byte id) { + this.midRed = midRed; + this.midGreen = midGreen; + this.midBlue = midBlue; + + this.parent = parent; + this.level = (byte) (parent == null ? 0 : parent.level + 1); + this.id = id; + } + } + + Node root; + + public static final int MAX_TREE_DEPTH = 8; + public static final int MAX_NODES = 266817; + public static final int MAX_RGB = 255; + + // prepare square tables + public static final int SQUARES[] = new int[MAX_RGB * 2 + 1]; + + public int shift[] = new int[MAX_TREE_DEPTH + 1]; + + static { + for (int i = -MAX_RGB; i <= MAX_RGB; ++i) + SQUARES[i + MAX_RGB] = i * i; + + } + + int depth; // depth of the tree + int nodes; // number of nodes in tree + int colors; // nodes of colors + + int pruningTreshold; + int nextPruningTreshold; + + int colormap[]; + + int cdistance; + int cred, cgreen, cblue; + int colorNumber; + + public Octree() { + root = new Node((MAX_RGB + 1) >> 1, (MAX_RGB + 1) >> 1, (MAX_RGB + 1) >> 1, null, (byte) 0); + + root.parent = root; + root.numberColors = Integer.MAX_VALUE; + } + + public void quantize(int pixels[], int width, int maxColors) { + + depth = 1; + for (int mc = maxColors; mc != 0; mc >>= 2, ++depth) + ; + + int numberPixels = pixels.length; + int maxShift; + + for (maxShift = 4 * 8; numberPixels != 0; maxShift--) + numberPixels >>= 1; + + for (int level = 0; level <= depth; level++) { + shift[level] = maxShift; + if (maxShift != 0) + maxShift--; + } + + sortInImage(pixels); + + reduction(maxColors); + assignment(pixels, maxColors); + } + + void sortInImage(int pixels[]) { + + int r, g, b, color; + for (int i = 0; i < pixels.length; ++i) { + color = pixels[i]; + r = (color >> 16) & 0xff; + g = (color >> 8) & 0xff; + b = color & 0xff; + sortInRGB(r, g, b); + } + } + + void sortInRGB(int red, int green, int blue) { + + // prune one level if tree is too large + if (nodes > MAX_NODES) { + pruneLevel(root); + --depth; + } + + // descent the tree, start with root + Node node = root; + + for (int level = 1; level <= depth; ++level) { + + int id = (red > node.midRed ? 1 : 0) | (green > node.midGreen ? 2 : 0) + | (blue > node.midBlue ? 4 : 0); + + // was this branch visited before ? + if (node.children[id] == null) { + int bisect = (1 << (MAX_TREE_DEPTH - level)) >> 1; + Node n = new Node(node.midRed + ((id & 1) != 0 ? bisect : -bisect), node.midGreen + + ((id & 2) != 0 ? bisect : -bisect), node.midBlue + + ((id & 4) != 0 ? bisect : -bisect), node, (byte) id); + ++nodes; + node.census |= 1 << id; // register new child + node.children[id] = n; + + if (level == depth) + ++colors; + } + + node = node.children[id]; // descent to next level + node.numberColors += 1 << shift[level]; + } + + ++node.numberUnique; + node.totalRed += red; + node.totalGreen += green; + node.totalBlue += blue; + } + + void pruneLevel(Node node) { + + if (node.census != 0) + for (int i = 0; i < node.children.length; ++i) + if ((node.census & (1 << i)) != 0) + pruneLevel(node.children[i]); + + if (node.level == depth) + pruneChild(node); + } + + void reduction(int numberColors) { + + nextPruningTreshold = 1; + + while (colors > numberColors) { + pruningTreshold = nextPruningTreshold; + nextPruningTreshold = root.numberColors; + colors = 0; + reduce(root); + } + } + + void reduce(Node node) { + + if (node.census != 0) + for (int i = 0; i < node.children.length; ++i) + if ((node.census & (1 << i)) != 0) + reduce(node.children[i]); + + if (node.numberColors <= pruningTreshold) + pruneChild(node); + else { + if (node.numberUnique > 0) + ++colors; + if (node.numberColors < nextPruningTreshold) + nextPruningTreshold = node.numberColors; + } + } + + void pruneChild(Node node) { + + Node parent = node.parent; + + // parent.children[node.id] = null; + parent.census &= ~(1 << node.id); + parent.numberUnique += node.numberUnique; + parent.totalRed += node.totalRed; + parent.totalGreen += node.totalGreen; + parent.totalBlue += node.totalBlue; + --nodes; + } + + void assignment(int pixels[], int maxColors) { + + colormap = new int[maxColors]; + colors = 0; + colorMap(root); + + int r, g, b, color; + + for (int i = 0; i < pixels.length; ++i) { + color = pixels[i]; + r = (color >> 16) & 0xff; + g = (color >> 8) & 0xff; + b = color & 0xff; + pixels[i] = colormap[rgb2idx(r, g, b)]; + } + } + + void colorMap(Node node) { + + if (node.census != 0) + for (int i = 0; i < node.children.length; ++i) + if ((node.census & (1 << i)) != 0) + colorMap(node.children[i]); + + if (node.numberUnique != 0) { + int uh = node.numberUnique >> 1; + int r = (node.totalRed + uh) / node.numberUnique; + int g = (node.totalGreen + uh) / node.numberUnique; + int b = (node.totalBlue + uh) / node.numberUnique; + + node.colorNumber = colors; + colormap[colors++] = (r << 16) | (g << 8) | b; + } + } + + int rgb2idx(int red, int green, int blue) { + + Node node = root; + + for (;;) { + byte id = (byte) ((red > node.midRed ? 1 : 0) | (green > node.midGreen ? 2 : 0) | (blue > node.midBlue ? 4 + : 0)); + + if ((node.census & (1 << id)) == 0) + break; + + node = node.children[id]; + } + + cdistance = Integer.MAX_VALUE; + cred = red; + cgreen = green; + cblue = blue; + + closestColor(node.parent); + + return colorNumber; + } + + void closestColor(Node node) { + if (node.census != 0) + for (int i = 0; i < node.children.length; i++) + if ((node.census & (1 << i)) != 0) + closestColor(node.children[i]); + + if (node.numberUnique != 0) { + + int color = colormap[node.colorNumber]; + int r = (color >> 16) & 0xff; + int g = (color >> 8) & 0xff; + int b = color & 0xff; + + int rD = r - cred + MAX_RGB; + int gD = g - cgreen + MAX_RGB; + int bD = b - cblue + MAX_RGB; + + int distance = SQUARES[rD] + SQUARES[gD] + SQUARES[bD]; + + if (distance < cdistance) { + cdistance = distance; + colorNumber = node.colorNumber; + } + } + } +} diff --git a/UI/src/nl/weeaboo/vnds/tools/SoundConverter.java b/UI/src/nl/weeaboo/vnds/tools/SoundConverter.java new file mode 100644 index 0000000..2197b64 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/SoundConverter.java @@ -0,0 +1,278 @@ +package nl.weeaboo.vnds.tools; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.string.StringUtil2; +import nl.weeaboo.system.ProcessUtil; +import nl.weeaboo.vnds.BatchProcess; +import nl.weeaboo.vnds.FileOp; +import nl.weeaboo.vnds.ProgressListener; + +public class SoundConverter { + + public enum ConvertType { + TYPE_AAC("AAC"), TYPE_ADPCM("ADPCM"), TYPE_MP3("MP3"), TYPE_OGG("OGG"); + + private String label; + + private ConvertType(String label) { + this.label = label; + } + + public String toString() { return label; } + + public static ConvertType fromExt(String string) { + string = string.toLowerCase(); + if (string.equals("aac")) { + return TYPE_AAC; + } else if (string.equals("adpcm")) { + return TYPE_ADPCM; + } else if (string.equals("mp3")) { + return TYPE_MP3; + } else if (string.equals("ogg")) { + return TYPE_OGG; + } + + throw new IllegalArgumentException("Unsupported conversion type: " + string); + } + } + + public static final int AAC_Q_LOW = 35; + public static final int AAC_Q_MED = 50; + public static final int AAC_Q_HIGH = 70; + + public static final int VORBIS_Q_LOW = 0; + public static final int VORBIS_Q_MED = 2; + public static final int VORBIS_Q_HIGH = 4; + + private String workingDir = "tools/"; + private int maxThreads = 8; + private ConvertType mode = ConvertType.TYPE_AAC; + private int aac_quality = AAC_Q_HIGH; + private int mp3_minb = 8, mp3_maxb = 128, mp3_avgb = 96; + private int vorbis_quality = VORBIS_Q_MED; + private int volume = 100; + private boolean nameToUpperCase; + private boolean log; + + public SoundConverter() { + } + + // Functions + public static void main(String args[]) throws IOException { + SoundConverter ve = new SoundConverter(); + + //Fate/Stay Night + //ve.convertFolder("foldername/"); + + //Narcissu + //ve.setVolume(800); + //ve.setConvertNameToUpperCase(true); + //ve.convertFolder("foldername/", null); + + ve.setMode(ConvertType.TYPE_OGG); + ve.convertFile(new File("z:/temp.mp3")); + } + + public void convertFolder(String folder, final ProgressListener pl) { + Map files = new HashMap(); + for (File file : new File(folder).listFiles()) { + if (!file.isDirectory()) files.put(file.getName(), file); + } + + BatchProcess bp = new BatchProcess(); + bp.setTaskSize(32); + bp.setThreads(maxThreads); + bp.setThreadPriority(Thread.MIN_PRIORITY); + bp.addProgressListener(pl); + try { + bp.run(files, new FileOp() { + public void execute(String relpath, File file) throws IOException { + convertFile(file); + } + }); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + protected static void collectFiles(Set files, File file, int depth) { + if (file.isDirectory()) { + if (depth == 0 && !file.getName().equals("special")) { + for (File f : file.listFiles()) { + collectFiles(files, f, depth + 1); + } + } + } else if (!file.getName().endsWith(".aac")) { + files.add(file); + } + } + + protected static File[] createTempFiles(File file, File tempFolder, String... exts) + throws IOException + { + File[] result = new File[exts.length + 1]; + + String path = file.getAbsolutePath(); + + //Check if entire path consists of ASCII characters + boolean ascii = true; + for (int n = 0; n < path.length(); n++) { + if (path.charAt(n) > 127) { + ascii = false; + break; + } + } + + if (ascii) { + result[0] = file; + } else { + File dst = null; + String pre = String.format("temp-" + Thread.currentThread().hashCode()); + String post = StringUtil.getExtension(file.getName()).toLowerCase(); + while (dst == null || dst.exists()) { + dst = new File(tempFolder, pre + "-" + StringUtil2.getRandomString(4) + "." + post); + } + FileUtil.copyFile(file, dst); + result[0] = dst; + } + + for (int n = 0; n < exts.length; n++) { + String p = StringUtil.replaceExt(result[n].getAbsolutePath(), exts[n]); + File f = new File(p); + + int t = 1; + while (f.exists()) { + t++; + f = new File(StringUtil.stripExtension(p) + "-" + t + "." + StringUtil.getExtension(p)); + } + result[n+1] = f; + } + + return result; + } + + public void convertFile(File file) throws IOException { + convertFile(file, mode, null); + } + public File convertFile(File srcF, File dstFolder) throws IOException { + return convertFile(srcF, mode, dstFolder); + } + public File convertFile(File srcF, ConvertType mode, File dstFolder) throws IOException { + if (dstFolder == null) { + dstFolder = srcF.getParentFile(); + } + dstFolder.mkdirs(); + + String filters = ""; + if (volume != 100) { + filters += " -vol " + volume; + } + + File[] temp; + String[] cmds; + switch (mode) { + case TYPE_AAC: + temp = createTempFiles(srcF, dstFolder, "wav", "aac"); + + cmds = new String[] { + String.format("ffmpeg -y -i \"%s\" -vn -ac 1 -ar 22050 %s \"%s\"", + temp[0], filters, temp[1]), + String.format("faac -q %d -o \"%s\" \"%s\"", + aac_quality, temp[2], temp[1]) + }; + break; + case TYPE_MP3: + temp = createTempFiles(srcF, dstFolder, "wav", "mp3"); + + String bitrateFlags = String.format("--abr %d -b %d -B %d", + mp3_avgb, mp3_minb, mp3_maxb); + + cmds = new String[] { + String.format("ffmpeg -y -i \"%s\" -vn -ac 2 -ar 32000 %s \"%s\"", + temp[0], filters, temp[1]), + String.format("lame --resample 32 %s \"%s\" \"%s\"", + bitrateFlags, temp[1], temp[2]) + }; + break; + case TYPE_ADPCM: + temp = createTempFiles(srcF, dstFolder, "wav", "wav.raw"); + + cmds = new String[] { + String.format("ffmpeg -y -i \"%s\" -vn -acodec adpcm_ima_wav -ac 1 -ar 22050 %s \"%s\"", + temp[0], filters, temp[1]), + String.format("ima2raw \"%s\"", + temp[1]) + }; + break; + case TYPE_OGG: + temp = createTempFiles(srcF, dstFolder, "ogg"); + + cmds = new String[] { + String.format("ffmpeg -y -i \"%s\" -vn -acodec libvorbis -aq %d %s \"%s\"", + temp[0], vorbis_quality, filters, temp[1]) + }; + break; + default: + throw new IllegalArgumentException("Illegal mode: " + mode); + } + + File dstF = new File(dstFolder, StringUtil.replaceExt(srcF.getName(), + StringUtil.getExtension(temp[temp.length-1].getName()).toLowerCase())); + try { + for (String cmd : cmds) { + Process p = ProcessUtil.execInDir(cmd, workingDir); + //System.out.println(ProcessUtil.read(p)); + ProcessUtil.waitFor(p); + } + } finally { + //Rename final result to dst + if (temp[temp.length-1].exists()) { + temp[temp.length-1].renameTo(dstF); + } + + //Delete temp files + for (File file : temp) { + if (!file.equals(srcF) && !file.equals(dstF)) { + file.delete(); + } + } + } + + return dstF; + } + + // Getters + public boolean isLogging() { return log; } + public ConvertType getMode() { return mode; } + public int getAacQuality() { return aac_quality; } + public int getVorbisQuality() { return vorbis_quality; } + public int getMp3MinBitrate() { return mp3_minb; } + public int getMp3MaxBitrate() { return mp3_maxb; } + public int getMp3AvgBitrate() { return mp3_avgb; } + public int getVolume() { return volume; } + public boolean getConvertNameToUpperCase() { return nameToUpperCase; } + public int getMaxThreads() { return maxThreads; } + public String getWorkingDir() { return workingDir; } + + // Setters + public void setMode(ConvertType mode) { this.mode = mode; } + public void setVorbisQuality(int quality) { this.vorbis_quality = quality; } + public void setAacQuality(int quality) { this.aac_quality = quality; } + public void setMp3Quality(int min, int max, int avg) { + mp3_minb = min; + mp3_maxb = max; + mp3_avgb = avg; + } + public void setVolume(int volume) { this.volume = volume; } + public void setConvertNameToUpperCase(boolean nameToUpperCase) { this.nameToUpperCase = nameToUpperCase; } + public void setMaxThreads(int mt) { this.maxThreads = mt; } + public void setWorkingDir(String dir) { this.workingDir = dir; } + +} diff --git a/UI/src/nl/weeaboo/vnds/tools/SoundConverterGUI.java b/UI/src/nl/weeaboo/vnds/tools/SoundConverterGUI.java new file mode 100644 index 0000000..ee29108 --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/SoundConverterGUI.java @@ -0,0 +1,201 @@ +package nl.weeaboo.vnds.tools; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.awt.AwtUtil; +import nl.weeaboo.awt.FileBrowseField; +import nl.weeaboo.awt.Sash; +import nl.weeaboo.vnds.ProgressListener; +import nl.weeaboo.vnds.ProgressRunnable; +import nl.weeaboo.vnds.VNDSProgressDialog; +import nl.weeaboo.vnds.tools.SoundConverter.ConvertType; + +/* + * Changes: + * + * 2009/11/08 -- v1.4.2 + * - Code cleanup + * - Converts input files even if they're already the correct file type + * + * 2008/03/03 -- v1.4.1 + * - Added support for changing MP3 quality + * + * 2008/10/11 -- v1.4 + * - Added MP3 support + * - Added a support for changing the number of threads used + * + * 2008/09/13 -- v1.3 + * - Bug sometimes caused end of soundfile to become corrupted + * + * 2008/07/25 -- v1.2 + * - Logging was broken, now fixed + * - Filenames with spaces in them weren't handled properly + * + * 2008/07/24 -- v1.1 + * - ADPCM Support + * - Logging function + * + * 2008/06/21 -- v1.0 + * - Initial Release + * + */ +@SuppressWarnings("serial") +public class SoundConverterGUI extends JFrame { + + private SoundConverter encoder; + + private JComboBox modeCombo; + private JSpinner aacQualitySpinner; + private JSpinner mp3QualitySpinner; + private JSpinner volumeSpinner; + private JCheckBox toUpperCheck; + private JSpinner threadsSpinner; + private FileBrowseField browseField; + private JButton convertButton; + + public SoundConverterGUI() { + encoder = new SoundConverter(); + + setTitle("VNDS Sound Converter v1.4.2"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + setLayout(new BorderLayout()); + add(createCenterPanel(), BorderLayout.CENTER); + + reset(); + + setMinimumSize(new Dimension(300, 100)); + pack(); + setLocationRelativeTo(null); + setVisible(true); + } + + //Functions + public static void main(String args[]) { + AwtUtil.setDefaultLAF(); + + new SoundConverterGUI(); + } + + protected JPanel createCenterPanel() { + JLabel modeLabel = new JLabel("Output Type"); + modeCombo = new JComboBox(new Object[] {ConvertType.TYPE_AAC, ConvertType.TYPE_ADPCM, + ConvertType.TYPE_MP3}); + modeCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + ConvertType mode = (ConvertType)modeCombo.getSelectedItem(); + encoder.setMode(mode); + aacQualitySpinner.setEnabled(mode == ConvertType.TYPE_AAC); + mp3QualitySpinner.setEnabled(mode == ConvertType.TYPE_MP3); + } + }); + + JLabel aacQualityLabel = new JLabel("AAC Quality"); + aacQualitySpinner = new JSpinner(new SpinnerNumberModel(70, 5, 995, 5)); + JLabel mp3QualityLabel = new JLabel("MP3 Bitrate"); + mp3QualitySpinner = new JSpinner(new SpinnerNumberModel(96, 8, 128, 16)); + JLabel volumeLabel = new JLabel("Volume (%)"); + volumeSpinner = new JSpinner(new SpinnerNumberModel(100, 10, 990, 10)); + JLabel toUpperLabel = new JLabel("Filenames to uppercase?"); + toUpperCheck = new JCheckBox(); + JLabel threadsLabel = new JLabel("Threads"); + threadsSpinner = new JSpinner(new SpinnerNumberModel(8, 1, 128, 1)); + + JPanel panel = new JPanel(new GridLayout(-1, 2, 15, 5)); + panel.add(modeLabel); panel.add(modeCombo); + panel.add(aacQualityLabel); panel.add(aacQualitySpinner); + panel.add(mp3QualityLabel); panel.add(mp3QualitySpinner); + panel.add(volumeLabel); panel.add(volumeSpinner); + panel.add(toUpperLabel); panel.add(toUpperCheck); + panel.add(threadsLabel); panel.add(threadsSpinner); + + browseField = FileBrowseField.readFolder("Folder", new File("")); + convertButton = new JButton("Convert"); + convertButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + convert(); + } + }); + + JPanel bottomPanel = new JPanel(new BorderLayout(10, 10)); + bottomPanel.add(new Sash(Sash.HORIZONTAL), BorderLayout.NORTH); + bottomPanel.add(browseField, BorderLayout.CENTER); + bottomPanel.add(convertButton, BorderLayout.EAST); + + JPanel panel2 = new JPanel(new BorderLayout(0, 10)); + panel2.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel2.add(panel, BorderLayout.NORTH); + panel2.add(bottomPanel, BorderLayout.SOUTH); + return panel2; + } + + public void convert() { + final File folder = browseField.getFile(); + if (!folder.exists() || !folder.isDirectory()) { + JOptionPane.showMessageDialog(null, "Invalid directory: \"" + folder + "\"", + "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + int mp3_avgb = ((Integer)aacQualitySpinner.getValue()) / 16 * 16; //round down to nearest 16 + int mp3_minb = encoder.getMp3MinBitrate(); + int mp3_maxb = ((mp3_avgb+15) / 16) * 16; //round up to nearest 16 + + mp3_avgb = Math.min(128, mp3_avgb); + mp3_maxb = Math.min(128, mp3_maxb); + + encoder.setAacQuality((Integer)aacQualitySpinner.getValue()); + encoder.setMp3Quality(mp3_minb, mp3_maxb, mp3_avgb); + encoder.setVolume((Integer)volumeSpinner.getValue()); + encoder.setConvertNameToUpperCase(toUpperCheck.isSelected()); + encoder.setMaxThreads((Integer)threadsSpinner.getValue()); + + ProgressListener pl = new ProgressListener() { + public void onFinished(String message) { + JOptionPane.showMessageDialog(null, String.format( + "Conversion finished."), + "Finished", JOptionPane.PLAIN_MESSAGE); + } + public void onProgress(int value, int max, String message) { + } + }; + + ProgressRunnable task = new ProgressRunnable() { + public void run(ProgressListener pl) { + encoder.convertFolder(folder.toString(), pl); + } + }; + + VNDSProgressDialog dialog = new VNDSProgressDialog(); + dialog.showDialog(task, pl); + } + + public void reset() { + aacQualitySpinner.setValue(encoder.getAacQuality()); + mp3QualitySpinner.setValue(encoder.getMp3AvgBitrate()); + volumeSpinner.setValue(encoder.getVolume()); + toUpperCheck.setSelected(encoder.getConvertNameToUpperCase()); + threadsSpinner.setValue(encoder.getMaxThreads()); + } + + //Getters + + //Setters + +} diff --git a/UI/src/nl/weeaboo/vnds/tools/TextureConverter.java b/UI/src/nl/weeaboo/vnds/tools/TextureConverter.java new file mode 100644 index 0000000..74eea1f --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/TextureConverter.java @@ -0,0 +1,545 @@ +package nl.weeaboo.vnds.tools; + +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel.MapMode; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; + +import javax.imageio.ImageIO; + +import nl.weeaboo.collections.Tuple2; +import nl.weeaboo.common.StringUtil; +import nl.weeaboo.io.FileUtil; +import nl.weeaboo.system.ProcessUtil; +import nl.weeaboo.vnds.BatchProcess; +import nl.weeaboo.vnds.FileOp; +import nl.weeaboo.vnds.ProgressListener; + +public class TextureConverter { + + /* + * Formats: + * [1] A3I5 + * [2] -- + * [3] -- + * [4] RGB256 + * [5] -- + * [6] A5I3 + * [7] RGBA + */ + + public enum Mode { + A3I5(1, "A3I5"), + RGB256(4, "RGB256"), + A5I3(6, "A5I3"), + RGBA(7, "RGBA"); + + private int num; + private String label; + + private Mode(int i, String l) { + num = i; + label = l; + } + + public static Mode fromInt(int i) { + for (Mode m : Mode.values()) { + if (m.num == i) return m; + } + return null; + } + public String toString() { + return num + ". " + label; + } + } + + public enum Quant { + NEUQUANT, OCTREE; + } + + private Mode mode = Mode.RGB256; + private Quant quant = Quant.OCTREE; + private boolean dithering = true; + private int threads = Runtime.getRuntime().availableProcessors(); + + //Functions + protected static void printUsage() { + System.err.printf( + "Usage: java -jar TextureConverter.jar file [out-folder]\n" + + "\n\tmode:" + + "\n\t-1 => A3I5" + + "\n\t-4 => RGB256" + + "\n\t-6 => A5I3" + + "\n\t-7 => RGBA" + + "\n\tflags:" + + "\n\t-nodither => No dithering" + + "\n\t-quant " + + "\n\t-threads " + + "\n"); + } + + public static void main(String args[]) { + TextureConverter tc = new TextureConverter(); + String filename = null; + String folder = null; + + try { + tc.setMode(Mode.fromInt(Integer.parseInt(args[0].substring(1)))); + + for (int n = 1; n < args.length; n++) { + if (args[n].startsWith("-nodither")) { + tc.setDithering(false); + } else if (args[n].startsWith("-quant")) { + if (!args[++n].equals("neuquant")) { + tc.setQuant(Quant.NEUQUANT); + } else { + tc.setQuant(Quant.OCTREE); + } + } else if (args[n].startsWith("-threads")) { + tc.setThreads(Integer.parseInt(args[++n])); + } else if (filename == null) { + filename = args[n]; + } else if (folder == null) { + folder = args[n]; + } else { + throw new IllegalArgumentException("Error parsing arg: " + args[n]); + } + } + } catch (RuntimeException re) { + printUsage(); + return; + } + if (filename == null) { + printUsage(); + return; + } + + if (folder == null) { + folder = filename; + } + + try { + tc.convertFile(filename, folder); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void convertFolder(String folder, ProgressListener pl) throws IOException { + Map files = new HashMap(); + final File folderF = new File(folder).getCanonicalFile(); + if (folderF.isDirectory()) { + for (File file : folderF.listFiles()) { + if (!file.isDirectory()) files.put(file.getName(), file); + } + } else { + files.put(folderF.getName(), folderF); + } + + BatchProcess bp = new BatchProcess(); + bp.setTaskSize(32); + bp.setThreads(threads); + bp.setThreadPriority(Thread.MIN_PRIORITY); + bp.addProgressListener(pl); + try { + bp.run(files, new FileOp() { + public void execute(String relpath, File file) throws IOException { + convertFile(file.getAbsolutePath(), folderF.getAbsolutePath()); + } + }); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void convertFile(File file) throws IOException { + convertFile(file.getAbsolutePath(), file.getParent()); + } + public void convertFile(String src, String dst) throws IOException { + File srcF = new File(src); + File dstF = new File(dst); + + if (srcF == null || !srcF.exists() || !srcF.isFile() || !srcF.canRead()) { + throw new FileNotFoundException(srcF.getPath()); + } + + if (dstF.exists() && !dstF.isDirectory()) { + dstF = dstF.getParentFile(); + } else { + dst = dstF.getAbsolutePath()+File.separator+srcF.getName(); + } + dstF.mkdirs(); + + File temp = File.createTempFile("_tc" + Thread.currentThread().hashCode(), ".png", dstF); + try { + FileUtil.copyFile(srcF, temp); + String tempPath = temp.getAbsolutePath(); + + if (mode == Mode.A3I5) { + quantize(tempPath, 32); + convertToPalette(tempPath, 3, 5); + } else if (mode == Mode.RGB256) { + quantize(tempPath, 256); + convertToPalette(tempPath, 0, 8); + } else if (mode == Mode.A5I3) { + quantize(tempPath, 8); + convertToPalette(tempPath, 5, 3); + } else if (mode == Mode.RGBA) { + quantize(tempPath, 256); + convertToRAW(tempPath); + } else { + throw new IllegalArgumentException("Invalid mode: " + mode); + } + + String tempBase = StringUtil.stripExtension(tempPath); + String dstBase = StringUtil.stripExtension(dst); + + //Rename to dst names + + File dstDTA = new File(dstBase+".dta"); + File dstPAL = new File(dstBase+".pal"); + File tempDTA = new File(tempBase+".dta"); + File tempPAL = new File(tempBase+".pal"); + + dstDTA.delete(); + tempDTA.renameTo(dstDTA); + tempDTA.delete(); + + dstPAL.delete(); + tempPAL.renameTo(dstPAL); + tempPAL.delete(); + } finally { + temp.delete(); + } + } + + protected void quantize(String path, int numColors) throws IOException { + File file = new File(path).getCanonicalFile(); + if (file == null || !file.exists() || !file.isFile() || !file.canRead()) { + throw new FileNotFoundException(path); + } + path = file.getAbsolutePath(); + + //Dither to DS colors + BufferedImage image = ImageIO.read(file); + int iw = image.getWidth(); + int ih = image.getHeight(); + + int argb[] = image.getRGB(0, 0, iw, ih, null, 0, iw); + int alpha[] = new int[argb.length]; + for (int n = 0; n < argb.length; n++) { + alpha[n] = argb[n] >>> 24; + argb[n] = 0xFF000000 | (argb[n] & 0xFFFFFF); + } + + if (dithering) { + floydSteinberg(argb, iw, ih); + } + + //Quantize to the number of colors we want + if (quant == Quant.NEUQUANT) { + image.setRGB(0, 0, iw, ih, argb, 0, iw); + ImageIO.write(image, "png", file); + + Process p = ProcessUtil.execInDir(String.format( + "pngnq \"%s\" -n %d", path, numColors), + "."); + ProcessUtil.waitFor(p); + ProcessUtil.kill(p); + file.delete(); + new File(StringUtil.stripExtension(path)+"-nq8.png").renameTo(file); + + image = ImageIO.read(file); + image.getRGB(0, 0, image.getWidth(), image.getHeight(), + argb, 0, image.getWidth()); + } else if (quant == Quant.OCTREE) { + Octree tree = new Octree(); + tree.quantize(argb, iw, numColors); + } + + //Re-add alpha channel + for (int n = 0; n < argb.length; n++) { + argb[n] = (argb[n] & 0xFFFFFF) | (alpha[n]<<24); + } + + image = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB); + image.setRGB(0, 0, iw, ih, argb, 0, iw); + ImageIO.write(image, "png", file); + + //FileUtil.copyFile(file, new File(file.getParent()+"/temp2.png")); + } + + protected void convertToPalette(String path, int bitsA, int bitsC) throws IOException { + File src = new File(path); + BufferedImage image = ImageIO.read(src); + int argb[] = image.getRGB(0, 0, image.getWidth(), image.getHeight(), + null, 0, image.getWidth()); + src.delete(); + + int maxA = (1<>24) & 0xFF; + int idx = lookupPalette(palette, A8R8G8B8_to_A1B5G5R5(c)); + if (alpha == 0) idx = 0; + + c = ((Math.round(maxA * alpha / 255f) & maxA) << bitsC) | idx; + bout.write(c&0xFF); + } + bout.flush(); + bout.close(); + + File dstPAL = new File(StringUtil.stripExtension(path)+".pal"); + bout = new BufferedOutputStream(new FileOutputStream(dstPAL)); + for (int c : palette) { + bout.write(c&0xFF); + bout.write((c>>8)&0xFF); + } + bout.flush(); + bout.close(); + } + + + protected void convertToRAW(String path) throws IOException { + File src = new File(path); + BufferedImage image = ImageIO.read(src); + int argb[] = image.getRGB(0, 0, image.getWidth(), image.getHeight(), + null, 0, image.getWidth()); + A8R8G8B8_to_A1B5G5R5(argb); + src.delete(); + + File dst = new File(StringUtil.stripExtension(path)+".dta"); + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(dst)); + for (int c : argb) { + bout.write(c&0xFF); + bout.write((c>>8)&0xFF); + } + bout.flush(); + bout.close(); + } + + protected int A8R8G8B8_to_A1B5G5R5(int c) { + int a = ((c>>>24) >= 127 ? (1<<15) : 0); + int r = Math.round(((c>>16)&0xFF) / 8f); + r = Math.max(0, Math.min(31, r)); + int g = Math.round(((c>>8)&0xFF) / 8f); + g = Math.max(0, Math.min(31, g)); + int b = Math.round(((c)&0xFF) / 8f); + b = Math.max(0, Math.min(31, b)); + return a | (b<<10) | (g<<5) | (r); + } + protected void A8R8G8B8_to_A1B5G5R5(int data[]) { + for (int n = 0; n < data.length; n++) { + data[n] = A8R8G8B8_to_A1B5G5R5(data[n]); + } + } + + protected int[] createPalette(int argb[], int maxC) { + int colorHistogram[] = new int[0x8000]; + for (int c : argb) { + colorHistogram[A8R8G8B8_to_A1B5G5R5(c) & 0x7FFF]++; + } + + TreeSet> set = new TreeSet>( + new Comparator>() { + public int compare(Tuple2 t1, Tuple2 t2) { + int c = t1.y.compareTo(t2.y); + if (c == 0) c = t1.x.compareTo(t2.x); + return -c; + } + } + ); + + for (int n = 0; n < colorHistogram.length; n++) { + if (colorHistogram[n] > 0) { + set.add(Tuple2.newTuple(n, colorHistogram[n])); + } + } + + int result[] = new int[maxC]; + int t = 1; + for (Tuple2 tuple : set) { + if (t >= result.length) break; + + result[t++] = tuple.x; + } + + return result; + } + + protected int lookupPalette(int palette[], int c0) { + int b0 = (c0>>10) & 31; + int g0 = (c0>>5) & 31; + int r0 = (c0) & 31; + + int best = 0; + int bestDist = Integer.MAX_VALUE; + + for (int idx = 1; idx < palette.length; idx++) { + int c1 = palette[idx]; + int b1 = (c1>>10) & 31; + int g1 = (c1>>5) & 31; + int r1 = (c1) & 31; + + int dist = Math.abs(r1-r0) + Math.abs(g1-g0) + Math.abs(b1-b0); + if (dist < bestDist) { + best = idx; + bestDist = dist; + } + } + + return best; + } + + /** Does dithering for 5-bit colors (equivalent to 8-bit / 8.0) */ + private static void floydSteinberg(int rgb[], int w, int h) { + double rgba[][][] = new double[w][h][4]; + int t = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + rgba[x][y][0] = ((rgb[t]>>>16)&0xFF); + rgba[x][y][1] = ((rgb[t]>>>8 )&0xFF); + rgba[x][y][2] = ((rgb[t] )&0xFF); + rgba[x][y][3] = ((rgb[t]>>>24)&0xFF); + + t++; + } + } + + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + for (int c = 0; c < 3; c++) { + double oldc = rgba[x][y][c]; + double newc = rgba[x][y][c] = Math.round(oldc / 8.0) * 8.0; + double error = oldc - newc; + + if (x+1 < w) { + rgba[x+1][y][c] += (7.0 * error) / 16.0; + rgba[x+1][y][c] = Math.min(255.0, Math.max(0.0, rgba[x+1][y][c])); + } + if (y+1 < h) { + if (x-1 >= 0) { + rgba[x-1][y+1][c] += (3.0 * error) / 16.0; + rgba[x-1][y+1][c] = Math.min(255.0, Math.max(0.0, rgba[x-1][y+1][c])); + } + rgba[x][y+1][c] += (5.0 * error) / 16.0; + rgba[x][y+1][c] = Math.min(255.0, Math.max(0.0, rgba[x][y+1][c])); + if (x+1 < w) { + rgba[x+1][y+1][c] += (1.0 * error) / 16.0; + rgba[x+1][y+1][c] = Math.min(255.0, Math.max(0.0, rgba[x+1][y+1][c])); + } + } + } + } + } + + t = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int a = Math.min(255, Math.max(0, (int)Math.round(rgba[x][y][3]))); + int r = Math.min(255, Math.max(0, (int)Math.round(rgba[x][y][0]))); + int g = Math.min(255, Math.max(0, (int)Math.round(rgba[x][y][1]))); + int b = Math.min(255, Math.max(0, (int)Math.round(rgba[x][y][2]))); + rgb[t] = (a<<24)|(r<<16)|(g<<8)|(b); + t++; + } + } + } + + public Image readImage(String origPath) throws IOException { + File origF = new File(origPath); + BufferedImage orig = ImageIO.read(origF); + int w = orig.getWidth(); + int h = orig.getHeight(); + + String base = StringUtil.stripExtension(origPath); + + File convDTA = new File(base + ".dta"); + FileInputStream dtaIn = new FileInputStream(convDTA); + + try { + ByteBuffer dtaBuf = dtaIn.getChannel().map(MapMode.READ_ONLY, 0, convDTA.length()); + dtaBuf.order(ByteOrder.LITTLE_ENDIAN); + + int bpp = (mode == Mode.RGBA ? 2 : 1); + int pixels[] = new int[((int)convDTA.length()) / bpp]; + for (int n = 0; n < pixels.length; n++) { + if (bpp == 2) pixels[n] = dtaBuf.getShort(); + else pixels[n] = dtaBuf.get(); + } + dtaIn.close(); + + if (mode != Mode.RGBA) { + File convPAL = new File(base + ".pal"); + FileInputStream palIn = new FileInputStream(convPAL); + try { + ByteBuffer palBuf = palIn.getChannel().map(MapMode.READ_ONLY, 0, convPAL.length()); + palBuf.order(ByteOrder.LITTLE_ENDIAN); + int palette[] = new int[((int)convPAL.length()) / 2]; + for (int n = 0; n < palette.length; n++) { + palette[n] = palBuf.getShort(); + } + + int colorMask = 0xFF; + if (mode == Mode.A3I5) colorMask = 31; + else if (mode == Mode.A5I3) colorMask = 7; + + for (int n = 0; n < pixels.length; n++) { + int c = pixels[n]; + + int alpha = 255; + if (mode == Mode.A3I5) alpha = (c>>5)*32+16; + else if (mode == Mode.A5I3) alpha = (c>>3)*8+4; + else if (mode == Mode.RGB256) alpha = (c == 0 ? 0 : 255); + + c = palette[c & colorMask]; + c = (((c&31)*8+4)<<16) | ((((c>>5)&31)*8+4)<<8) | (((c>>10)&31)*8+4); + pixels[n] = (alpha<<24) | c; + } + } finally { + palIn.close(); + } + } else { + for (int n = 0; n < pixels.length; n++) { + int c = pixels[n]; + int alpha = (c>>15 != 0 ? 255 : 0); + c = (((c&31)*8+4)<<16) | ((((c>>5)&31)*8+4)<<8) | (((c>>10)&31)*8+4); + pixels[n] = (alpha<<24) | c; + } + } + + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + image.setRGB(0, 0, w, h, pixels, 0, w); + return image; + } finally { + dtaIn.close(); + } + } + + //Getters + public Mode getMode() { return mode; } + public Quant getQuant() { return quant; } + public boolean isDithering() { return dithering; } + public int getThreads() { return threads; } + + //Setters + public void setMode(Mode mode) { this.mode = mode; } + public void setQuant(Quant quant) { this.quant = quant; } + public void setDithering(boolean dithering) { this.dithering = dithering; } + public void setThreads(int threads) { this.threads = threads; } + +} diff --git a/UI/src/nl/weeaboo/vnds/tools/TextureConverterGUI.java b/UI/src/nl/weeaboo/vnds/tools/TextureConverterGUI.java new file mode 100644 index 0000000..c0f15ea --- /dev/null +++ b/UI/src/nl/weeaboo/vnds/tools/TextureConverterGUI.java @@ -0,0 +1,226 @@ +package nl.weeaboo.vnds.tools; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.Image; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; +import java.util.List; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.border.EmptyBorder; + +import nl.weeaboo.awt.AwtUtil; +import nl.weeaboo.awt.FileBrowseField; +import nl.weeaboo.awt.Sash; +import nl.weeaboo.vnds.Log; +import nl.weeaboo.vnds.ProgressListener; +import nl.weeaboo.vnds.ProgressRunnable; +import nl.weeaboo.vnds.VNDSProgressDialog; +import nl.weeaboo.vnds.tools.TextureConverter.Mode; + +/* + * Changes: + * + * 2009/11/?? -- v1.0 + * - Initial release + * + */ +@SuppressWarnings("serial") +public class TextureConverterGUI extends JFrame { + + private TextureConverter converter; + + private JComboBox modeCombo; + private JCheckBox ditheringCheck; + private JSpinner threadsSpinner; + private FileBrowseField browseField; + private JButton convertButton; + + public TextureConverterGUI() { + converter = new TextureConverter(); + + setTitle("NDS Texture Converter v1.0"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + setLayout(new BorderLayout()); + add(createCenterPanel(), BorderLayout.CENTER); + + setMinimumSize(new Dimension(300, 100)); + pack(); + setLocationRelativeTo(null); + setVisible(true); + } + + //Functions + public static void main(String args[]) { + AwtUtil.setDefaultLAF(); + + if (args.length > 0) { + TextureConverter.main(args); + } else { + new TextureConverterGUI(); + } + } + + protected JPanel createCenterPanel() { + JLabel modeLabel = new JLabel("Format"); + modeCombo = new JComboBox(TextureConverter.Mode.values()); + modeCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Mode mode = (Mode)modeCombo.getSelectedItem(); + converter.setMode(mode); + } + }); + modeCombo.setSelectedItem(converter.getMode()); + + JLabel ditheringLabel = new JLabel("Dithering"); + ditheringCheck = new JCheckBox(); + ditheringCheck.setSelected(converter.isDithering()); + + JLabel threadsLabel = new JLabel("Threads"); + threadsSpinner = new JSpinner(new SpinnerNumberModel(converter.getThreads(), 1, 128, 1)); + + JPanel panel = new JPanel(new GridLayout(-1, 2, 15, 5)); + panel.add(modeLabel); panel.add(modeCombo); + panel.add(ditheringLabel); panel.add(ditheringCheck); + panel.add(threadsLabel); panel.add(threadsSpinner); + + browseField = FileBrowseField.readFolder("Folder", new File("")); + convertButton = new JButton("Convert"); + convertButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + convert(); + } + }); + + JPanel bottomPanel = new JPanel(new BorderLayout(10, 10)); + bottomPanel.add(new Sash(Sash.HORIZONTAL), BorderLayout.NORTH); + bottomPanel.add(browseField, BorderLayout.CENTER); + bottomPanel.add(convertButton, BorderLayout.EAST); + + JPanel panel2 = new JPanel(new BorderLayout(0, 10)); + panel2.setBorder(new EmptyBorder(10, 10, 10, 10)); + panel2.add(panel, BorderLayout.NORTH); + panel2.add(bottomPanel, BorderLayout.SOUTH); + + new DropTarget(panel2, new DropTargetListener() { + public void dragEnter(DropTargetDragEvent dtde) { + } + public void dragExit(DropTargetEvent dte) { + } + public void dragOver(DropTargetDragEvent dtde) { + for (DataFlavor df : dtde.getCurrentDataFlavorsAsList()) { + if (df.isFlavorJavaFileListType()) { + dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE); + break; + } + } + } + @SuppressWarnings("unchecked") + public void drop(DropTargetDropEvent dtde) { + dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); + + Transferable t = dtde.getTransferable(); + for (DataFlavor df : t.getTransferDataFlavors()) { + if (df.isFlavorJavaFileListType()) { + try { + List list = (List)t.getTransferData(df); + convert(list); + } catch (UnsupportedFlavorException e) { + Log.w("Drag&Drop Exception", e); + } catch (IOException e) { + Log.w("Drag&Drop Exception", e); + } + } + } + } + public void dropActionChanged(DropTargetDragEvent dtde) { + } + }); + + return panel2; + } + + public void convert() { + convert(null); + } + public void convert(final List files) { + final File folder = browseField.getFile(); + if (files == null) { + if (!folder.exists() || !folder.isDirectory()) { + JOptionPane.showMessageDialog(null, "Invalid directory: \"" + folder + "\"", + "Error", JOptionPane.ERROR_MESSAGE); + return; + } + } + + converter.setMode((Mode)modeCombo.getSelectedItem()); + converter.setDithering(ditheringCheck.isSelected()); + converter.setThreads((Integer)threadsSpinner.getValue()); + + ProgressListener pl = new ProgressListener() { + public void onFinished(String message) { + JOptionPane.showMessageDialog(null, "Conversion finished.", + "Finished", JOptionPane.PLAIN_MESSAGE); + } + public void onProgress(int value, int max, String message) { + } + }; + + ProgressRunnable task = new ProgressRunnable() { + public void run(ProgressListener pl) { + try { + if (files == null) { + converter.convertFolder(folder.toString(), pl); + } else { + for (File f : files) { + converter.convertFile(f); + } + if (files.size() == 1) { + Image image = converter.readImage(files.get(0).getAbsolutePath()); + JLabel label = new JLabel(new ImageIcon(image)); + + JFrame frame = new JFrame(); + frame.getContentPane().add(label); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }; + + VNDSProgressDialog dialog = new VNDSProgressDialog(); + dialog.showDialog(task, pl); + } + + //Getters + + //Setters + +} diff --git a/UI/template/fate/background/special/blackout.jpg b/UI/template/fate/background/special/blackout.jpg new file mode 100644 index 0000000..2845f6a Binary files /dev/null and b/UI/template/fate/background/special/blackout.jpg differ diff --git a/UI/template/fate/background/special/blueout.jpg b/UI/template/fate/background/special/blueout.jpg new file mode 100644 index 0000000..0a17330 Binary files /dev/null and b/UI/template/fate/background/special/blueout.jpg differ diff --git a/UI/template/fate/background/special/fate/bone1.jpg b/UI/template/fate/background/special/fate/bone1.jpg new file mode 100644 index 0000000..d7e7c46 Binary files /dev/null and b/UI/template/fate/background/special/fate/bone1.jpg differ diff --git a/UI/template/fate/background/special/fate/bone2.jpg b/UI/template/fate/background/special/fate/bone2.jpg new file mode 100644 index 0000000..8b1394f Binary files /dev/null and b/UI/template/fate/background/special/fate/bone2.jpg differ diff --git a/UI/template/fate/background/special/fate/bone3.jpg b/UI/template/fate/background/special/fate/bone3.jpg new file mode 100644 index 0000000..09ceee2 Binary files /dev/null and b/UI/template/fate/background/special/fate/bone3.jpg differ diff --git a/UI/template/fate/background/special/fate/bone4.jpg b/UI/template/fate/background/special/fate/bone4.jpg new file mode 100644 index 0000000..9b3b1ca Binary files /dev/null and b/UI/template/fate/background/special/fate/bone4.jpg differ diff --git a/UI/template/fate/background/special/fate/bone5.jpg b/UI/template/fate/background/special/fate/bone5.jpg new file mode 100644 index 0000000..6454b1c Binary files /dev/null and b/UI/template/fate/background/special/fate/bone5.jpg differ diff --git a/UI/template/fate/background/special/fate/bone6.jpg b/UI/template/fate/background/special/fate/bone6.jpg new file mode 100644 index 0000000..d4cc503 Binary files /dev/null and b/UI/template/fate/background/special/fate/bone6.jpg differ diff --git a/UI/template/fate/background/special/fate/bone7.jpg b/UI/template/fate/background/special/fate/bone7.jpg new file mode 100644 index 0000000..ff4442c Binary files /dev/null and b/UI/template/fate/background/special/fate/bone7.jpg differ diff --git a/UI/template/fate/background/special/fate/bone8.jpg b/UI/template/fate/background/special/fate/bone8.jpg new file mode 100644 index 0000000..bc3f506 Binary files /dev/null and b/UI/template/fate/background/special/fate/bone8.jpg differ diff --git a/UI/template/fate/background/special/greenout.jpg b/UI/template/fate/background/special/greenout.jpg new file mode 100644 index 0000000..df7f111 Binary files /dev/null and b/UI/template/fate/background/special/greenout.jpg differ diff --git a/UI/template/fate/background/special/redout.jpg b/UI/template/fate/background/special/redout.jpg new file mode 100644 index 0000000..4e2fbe2 Binary files /dev/null and b/UI/template/fate/background/special/redout.jpg differ diff --git a/UI/template/fate/background/special/tigerdojo.jpg b/UI/template/fate/background/special/tigerdojo.jpg new file mode 100644 index 0000000..2ddf490 Binary files /dev/null and b/UI/template/fate/background/special/tigerdojo.jpg differ diff --git a/UI/template/fate/background/special/title.jpg b/UI/template/fate/background/special/title.jpg new file mode 100644 index 0000000..48dc2c5 Binary files /dev/null and b/UI/template/fate/background/special/title.jpg differ diff --git a/UI/template/fate/background/special/title2.jpg b/UI/template/fate/background/special/title2.jpg new file mode 100644 index 0000000..94fbcc8 Binary files /dev/null and b/UI/template/fate/background/special/title2.jpg differ diff --git a/UI/template/fate/background/special/whiteout.jpg b/UI/template/fate/background/special/whiteout.jpg new file mode 100644 index 0000000..72513b7 Binary files /dev/null and b/UI/template/fate/background/special/whiteout.jpg differ diff --git a/UI/template/fate/default.ttf b/UI/template/fate/default.ttf new file mode 100644 index 0000000..e47fc14 Binary files /dev/null and b/UI/template/fate/default.ttf differ diff --git a/UI/template/fate/icon-high.png b/UI/template/fate/icon-high.png new file mode 100644 index 0000000..316ac2d Binary files /dev/null and b/UI/template/fate/icon-high.png differ diff --git a/UI/template/fate/icon.png b/UI/template/fate/icon.png new file mode 100644 index 0000000..f27666c Binary files /dev/null and b/UI/template/fate/icon.png differ diff --git a/UI/template/fate/info-ch.txt b/UI/template/fate/info-ch.txt new file mode 100644 index 0000000..9dee79c --- /dev/null +++ b/UI/template/fate/info-ch.txt @@ -0,0 +1,2 @@ +title=Fate/Stay Night +fontsize=16 diff --git a/UI/template/fate/info-en.txt b/UI/template/fate/info-en.txt new file mode 100644 index 0000000..6ed96bd --- /dev/null +++ b/UI/template/fate/info-en.txt @@ -0,0 +1 @@ +title=Fate/Stay Night diff --git a/UI/template/fate/info-ja.txt b/UI/template/fate/info-ja.txt new file mode 100644 index 0000000..9dee79c --- /dev/null +++ b/UI/template/fate/info-ja.txt @@ -0,0 +1,2 @@ +title=Fate/Stay Night +fontsize=16 diff --git a/UI/template/fate/instructions.txt b/UI/template/fate/instructions.txt new file mode 100644 index 0000000..a816764 --- /dev/null +++ b/UI/template/fate/instructions.txt @@ -0,0 +1,18 @@ + +There are two ways of installing, the basic install contains all the routes and voices. +The advanced installation method allows you to select which routes you want to install. +Installing only one route reduces the install size from 130MB to 75MB~100MB. + +Basic Installation +1. Copy the contents of "base_install" to /vnds/novels/fate on your flashcard. +2. Optional. Overwrite the script.zip and info.txt on the DS with the ones in "patch-ja" to play the game in Japanese. +3. Run with vnds.nds (http://www.digital-haze.net/vnds.php) + +Advanced installation +1. Run "FateInstaller.jar" (you may need to install Java first: http://java.com). +2. Select the routes you want to install, press install and choose a temporary install folder on your pc. +3. Copy the generated "fate" folder to /vnds/novels/ on your flashcard +4. Optional. Overwrite the script.zip and info.txt on the DS with the ones in "patch-ja" to play the game in Japanese. +5. Run with vnds.nds (http://www.digital-haze.net/vnds.php) + +Go to http://www.weeaboo.nl for updates and more information diff --git a/UI/template/fate/save/GLOBAL.SAV b/UI/template/fate/save/GLOBAL.SAV new file mode 100644 index 0000000..5d69d8d --- /dev/null +++ b/UI/template/fate/save/GLOBAL.SAV @@ -0,0 +1,4 @@ + + + + diff --git a/UI/template/fate/script/main.scr b/UI/template/fate/script/main.scr new file mode 100644 index 0000000..a902799 --- /dev/null +++ b/UI/template/fate/script/main.scr @@ -0,0 +1,24 @@ +sound ~ +music ~ +setvar ~ ~ +text ~ +bgload special/title2.jpg +text @Fate/Stay Night DS v1.1 +text @Translation by Mirror Moon +text @VNDS Port: anonl +delay 5 +music special/title.mp3 +delay 5 +text ! +choice Watch Prologue|Start Game +#choice Watch Prologue|Start Game|Mou Ikkai +music ~ +if selected == 1 + jump prologue00.scr +fi +if selected == 2 + jump fate01-00.scr +fi +if selected == 3 + jump mouikkai.scr +fi diff --git a/UI/template/fate/script/mouikkai.scr b/UI/template/fate/script/mouikkai.scr new file mode 100644 index 0000000..7ba1949 --- /dev/null +++ b/UI/template/fate/script/mouikkai.scr @@ -0,0 +1,313 @@ +music special/mouikkai.mp3 +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +delay 6 +bgload special/mouikkai1.png +delay 6 +bgload special/mouikkai2.png +delay 6 +bgload special/mouikkai3.png +delay 6 +bgload special/mouikkai4.png +music ~ +choice Mou Ikkai!|Back to Title +if selected == 1 + jump mouikkai.scr +fi +if selected == 2 + jump main.scr +fi diff --git a/UI/template/fate/script/ulw.scr b/UI/template/fate/script/ulw.scr new file mode 100644 index 0000000..d40400a --- /dev/null +++ b/UI/template/fate/script/ulw.scr @@ -0,0 +1,11 @@ +text I am the bone of my leek. +text Leeks are my body, and leeks are my blood. +text I have wielded over a thousand leeks. +text Unaware of loss. +text Nor aware of gain. +text Withstood pain to use many leeks. +text Waiting for one's arrival. +text I have no regrets, this is the only path. +text My whole life was "Unlimited Leek Works." +text You took an unimplemented path, returning to title screen... +jump main.scr diff --git a/UI/template/fate/sound/special/title.mp3 b/UI/template/fate/sound/special/title.mp3 new file mode 100644 index 0000000..7e45d38 Binary files /dev/null and b/UI/template/fate/sound/special/title.mp3 differ diff --git a/UI/template/fate/thumbnail-high.jpg b/UI/template/fate/thumbnail-high.jpg new file mode 100644 index 0000000..19b3d57 Binary files /dev/null and b/UI/template/fate/thumbnail-high.jpg differ diff --git a/UI/template/fate/thumbnail.png b/UI/template/fate/thumbnail.png new file mode 100644 index 0000000..c52e045 Binary files /dev/null and b/UI/template/fate/thumbnail.png differ diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4bf0e9f --- /dev/null +++ b/build.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +F=$1 + +if [ "$F" == "build" ]; then + rm -rf Out + + cd ./UI && ant && cd ../ + + cp -ra Distrib Out + + cp UI/FSN* Out/ + cp -ra UI/lib Out/lib + cp -ra UI/template Out/template + + mkdir tmp && cd tmp + wget http://unclemion.com/dev/attachments/download/43/onscrtools-linux-x86_64-20110930.tar.bz2 + tar -xvpf onscrtools-linux-x86_64-20110930.tar.bz2 + cd .. + + cp -a tmp/onscrtools-linux-x86_64-20110930/bin/* Out/bin/ + + rm -rf tmp + + cd Tools/ + + cd ahx2wav + chmod +x make.sh + ./make.sh ahx2wav + mv ahx2wav ../../Out/bin/ + + cd ../ima2raw + g++ -o ima2raw ima2raw.cpp + mv ima2raw ../../Out/bin/ + + cd ../tlg2bmp + g++ -o tlg2bmp tlg2bmp.cpp + mv tlg2bmp ../../Out/bin/ +elif [ "$F" == "clean" ]; then + rm -rf Out + cd Tools/ahx2wav + ./make.sh clean + cd ../../UI + ant clean + cd .. +fi