diff options
author | Livio Recchia <recchialivio@libero.it> | 2020-02-10 23:06:34 +0100 |
---|---|---|
committer | Livio Recchia <recchialivio@libero.it> | 2020-02-10 23:06:34 +0100 |
commit | 9a13903a2f7d3a65fdf15a65fb59cccd622e2066 (patch) | |
tree | 9403b7dff39eb5e5d7fa0f79efb69b496add4c4b | |
parent | 11cc316b74d5f3f283413a33e7693b314741aa4a (diff) | |
download | manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.tar.gz manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.tar.bz2 manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.tar.xz manachat-9a13903a2f7d3a65fdf15a65fb59cccd622e2066.zip |
Initial commit
321 files changed, 26909 insertions, 0 deletions
@@ -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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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. + + <signature of Ty Coon>, 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/RULES4BOTS.md b/RULES4BOTS.md new file mode 100644 index 0000000..6ab69b6 --- /dev/null +++ b/RULES4BOTS.md @@ -0,0 +1,5 @@ +TMWA rules for bots: +- Do not spam +- Broadcast is forbidden by any means +- GM commands not allowed +- No autofollow diff --git a/__pycache__/itemdb.cpython-37.pyc b/__pycache__/itemdb.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..c59d855 --- /dev/null +++ b/__pycache__/itemdb.cpython-37.pyc diff --git a/__pycache__/loggers.cpython-37.pyc b/__pycache__/loggers.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..3982253 --- /dev/null +++ b/__pycache__/loggers.cpython-37.pyc diff --git a/__pycache__/monsterdb.cpython-37.pyc b/__pycache__/monsterdb.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..c32d91e --- /dev/null +++ b/__pycache__/monsterdb.cpython-37.pyc diff --git a/actor.py b/actor.py new file mode 100644 index 0000000..f963941 --- /dev/null +++ b/actor.py @@ -0,0 +1,29 @@ +import net.mapserv as mapserv +from net.common import distance + + +def find_nearest_being(name='', type='', ignored_ids=[], allowed_jobs=[]): + + if mapserv.beings_cache is None: + return None + + min_disance = 1000000 + px = mapserv.player_pos['x'] + py = mapserv.player_pos['y'] + nearest = None + + for b in mapserv.beings_cache.values(): + if b.id in ignored_ids: + continue + if name and b.name != name: + continue + if type and b.type != type: + continue + if allowed_jobs and b.job not in allowed_jobs: + continue + dist = distance(px, py, b.x, b.y) + if dist < min_disance: + min_disance = dist + nearest = b + + return nearest diff --git a/actor.pyc b/actor.pyc Binary files differnew file mode 100644 index 0000000..8cd7abf --- /dev/null +++ b/actor.pyc diff --git a/android.txt b/android.txt new file mode 100644 index 0000000..52e71ab --- /dev/null +++ b/android.txt @@ -0,0 +1,3 @@ +title = ManaChat +author = Joseph Botosh +orientation = portrait @@ -0,0 +1,7 @@ +#!/bin/bash +while : +do + echo "Loading manachat (abort with CTRL+C)" + ./simple.py + sleep 15 +done diff --git a/badge.py b/badge.py new file mode 100644 index 0000000..c0b5525 --- /dev/null +++ b/badge.py @@ -0,0 +1,32 @@ +import net.mapserv as mapserv +from logicmanager import logic_manager + +__all__ = ['is_shop', 'is_afk', 'is_idle'] + +is_shop = False +is_afk = False +is_idle = False + +badge_ts = 0 + + +def badge_logic(ts): + if mapserv.server is None: + return + + global badge_ts + + if ts > badge_ts + 30: + badge_ts = ts + emote = 0xC0 + if is_shop: + emote += 1 + if is_afk: + emote += 2 + if is_idle: + emote += 4 + if is_shop or is_afk or is_idle: + mapserv.cmsg_player_emote(emote) + + +logic_manager.add_logic(badge_logic) diff --git a/badge.pyc b/badge.pyc Binary files differnew file mode 100644 index 0000000..00b8e54 --- /dev/null +++ b/badge.pyc diff --git a/bot/XCOM.txt b/bot/XCOM.txt new file mode 100644 index 0000000..dd263d2 --- /dev/null +++ b/bot/XCOM.txt @@ -0,0 +1,17 @@ +Livio +Attilio il Ramarro +General Kruton +PiNG0 +PiNG1 +PiNG2 +Bi0haZarD +Gum-i +Depredador +Manatauro +ManatauroMage +mifritscher +Reactos +Justin Kaess +Hello=) +Nced +GnuLinux diff --git a/bot/admins.txt b/bot/admins.txt new file mode 100644 index 0000000..8cacaae --- /dev/null +++ b/bot/admins.txt @@ -0,0 +1,7 @@ +Livio +Micksha +Jesusalva +Prsm +Rill +Gumi +General Kruton diff --git a/bot/disliked.txt b/bot/disliked.txt new file mode 100644 index 0000000..78c3cd2 --- /dev/null +++ b/bot/disliked.txt @@ -0,0 +1,2 @@ +Dresden +kytty diff --git a/bot/friends.txt b/bot/friends.txt new file mode 100644 index 0000000..c2ebca5 --- /dev/null +++ b/bot/friends.txt @@ -0,0 +1,5 @@ +Livio +Arkhen +PiNG1 +Jesusalva +Prsm diff --git a/bot/ignored.txt b/bot/ignored.txt new file mode 100644 index 0000000..895dce0 --- /dev/null +++ b/bot/ignored.txt @@ -0,0 +1 @@ +WarriorWorld diff --git a/bot/jokes.txt b/bot/jokes.txt new file mode 100644 index 0000000..d1f6f59 --- /dev/null +++ b/bot/jokes.txt @@ -0,0 +1,93 @@ +Do you think I'm lazy? +I miss Confused Tree :( +I miss CrazyTree :( +I'm not telling you! +*sighs* +Powered with the latest annoying technology available. +And remember: never lend your toothbrush to a slime! By the way I will never lend it to none including Arkim. +Healing the injured Mouboo? Why? It tasted good. +Cactus drinks are more popular than coke here. That's why Mana World is more healthy. +From where chicken legs come from? +The Mana World is beautiful as dangerous as well: that's why even bats always carry a dagger with them. +The Mana World is beautiful as dangerous as well: bee will not kill you for their poison but for their weight. +The Mana World is beautiful as dangerous as well: spiders here are something never seen even in Australia. +The Mana World is beautiful as dangerous as well: trolls will not play jokes. +Pink flowers have an eye just like security cameras. +Terranite ore is a terranite's dropping. +Took a shower one day but dryied myself with the wrong towel and ended up in Hurnscald square. +Why Soul menhirs give you towels instead of souls and why Jackos give you souls instead of towels? +I'm afraid of Luviabot. +Inspector will not personally inspect. +I miss Hyperspace's chattering. +I wonder if Undead Tree escaped with a horse in a glass elevator... +It was Jack that chopped down Undead Tree? +Christmas tree is boring compared to Undead Tree. +I miss a lot of nice people here... I should learn to aim better. +Whoever said that the definition of insanity is doing the same thing over and over again and expecting different results has obviously never had to reboot a computer. —William Petersen +My old aunts would come and tease me at weddings 'Well Livio? Do you think you’ll be next?' We’ve settled this quickly once I’ve started doing the same to them at funerals. +I dreamed I was forced to eat a giant marshmallow. When I woke up my pillow was gone. +One of the most wonderful things in life is to wake up and enjoy a cuddle with somebody; unless you are in prison. +I managed to lose my rifle when I was in the army. I had to pay $855 to cover the loss. I’m starting to understand why a Navy captain always goes down with his ship. +A wife is like a hand grenade. Take off the ring and say good bye to your house. +A naked women robbed a bank. Nobody could remember her face. +I thought I’d tell you a good time travel joke – but you didn't like it. +So much has changed since my girlfriend told me we’re having a baby. For instance my name address and telephone number! +Why don't skeletons fight each other? They don't have the guts. +What do politicians and diapers have in common? Both should be changed regularly and both for the same reason. +Knock knock. Who’s there? The love of your life. Liar! Chocolate can’t speak! +Waiter I am outraged. There is one hair in my soup. And what do you expect for this price? A whole wig?! +Is Google a he or a she? A she no doubt because it won‘t let you finish your sentence without suggesting other ideas. +Job interview in a psychiatry: So you’re interested in working with us. What is your experience with mentally disturbed people? I’ve been on Facebook for 5 years now. Very good the job is yours. +Santa Claus: So what do you want for Christmas this year? Guy: I want a dragon! Santa Claus: C‘mon be realistic! Guy: Ok I want 100 millon GPs all the rares and every GM command unlocked! Santa Claus: Hmmm what color dragon do you want? +Boy do you pray before you eat? No why should I - my mom is a good cook. +In a shop: Give me a roll of toilet paper. What color would you like? White please I will color it myself… +Can a joke played on April 1st become a present for the New Year? Yes as there's exactly 9 months period. +A elderly man remembers the good old days: “when I was young my mom could send me to a shop with a single $ and I would bring back 5 pounds of potatoes 2 breads a bottle of milk a piece of cheese and 10 eggs. Nowadays that’s impossible: there are simply too many security cameras. +The following text messages were exchanged on a cold winters day. Wife: Windows frozen. Husband: Pour some warm water over them. Wife: Computer completely screwed up now. +What do you call security guards working outside Samsung shops? Guardians of the Galaxy. +What bow can't be tied? A rainbow! +Why couldn't the pirate play cards? Because he was sitting on the deck! +Hi. If you are the phone company I already sent the money. If you are my parents please send money. If you are my financial institution you didn’t lend me enough money. If you are my friends you owe me money. If you are a female don’t worry I have plenty of money. +Please leave a message. However you have the right to remain silent. Everything you say will be recorded and used by us. +Hi I’m Livio’s answering machine. What are you? +Last night a thief broke into my house and started looking for money so i woke up and joined him. +Nothing ruins a great Friday more than realizing it’s actually Wednesday. +Just had the strangest experience. The lady walking ahead of me sped up so I did she began running so I ran too she screamed so I screamed as well. I never even saw what we were running from. +I hate this hot weather I have to keep my windows closed because all my neighbour’s kids do is scream. I’m seriously considering giving them back. +Saw a man earlier today at the beach shouting “Help shark! Help!”. It made me laugh there’s no way he was ever gonna convince a shark to help him. +Overcome awkward silences on those first dates by bringing an air horn with you. +Mary had a little lamb fries salad and a bottle of coke. +My granddad used to be in the army until he ate all the rations. He was shot for desserting. +I asked to WildX “Please get me a newspaper.” “Don’t be silly” he replied “you can borrow my iPad.” That spider never knew what hit it. +If women could read minds men would get slapped every 2 seconds. +Finally managed to cure my dry skin problem I’ve stopped using towels. +I want to meet a girl who loves romantic walks because I don’t have a car or any money. +Do gun manuals have a “trouble shooting” section? +I often say to myself “I can’t believe that cloning machine worked!” +If you have a parrot and you don’t teach it to say: ”Help I’ve been turned into a parrot!” you’re wasting everyone’s time. +I went to the bookshop today to get a book about conspiracies and government control of the media but I couldn’t find anything. Coincidence? +My parents gave me a really cheap dictionary for my birthday yesterday. I couldn’t find the words to thank them. +Just seen a man on a tractor shouting about the end of the world. I think it was farmer Geddon. +Took my mother-in-law out last night. Loving my new sniper rifle. +Just drove to work in a $200000 vehicle. I love catching the bus. +This weekend you went to Las Vegas in a $20000 car and came back in a $200000 bus. +Chocolate comes from cocoa which comes from a tree which is a plant. Therefore chocolate is a type of salad. +Whatever you do always give 100% unless your donating blood. +A very panicky Emma bursts into her brother’s bedroom and shakes him awake, “Jeremy, come quick, there’s a mouse squeaking under my bed!!!!”. Jeremy yawns, “and what the heck should I do? Oil it?!” +“Your waffle iron isn’t working, dear!” “Please just stay away from my laptop grandma!!!” +I bet you 125851265228542 GP that you didn’t bother to read that number. You just cruised right over it, didn’t you? You didn’t even notice I put a letter in it. Well I didn’t – but you went and looked anyway. My, you are quite predictable! +Why is it that bicycles fall over so often? They are two-tired. +A man is walking in the desert with his horse and his dog when the dog says, “I can’t do this. I need water.” The man says, “I didn’t know dogs could talk.” The horse says, “Me neither!” +Why are skeletons so calm? Because nothing gets under their skin. +How do trees get online? They just log on! +Players treats me like God. They ignore my existence and only talks to me when they needs something. +I don’t know if liquor is the answer, but it’s worth a shot! +If you want to catch a squirrel just climb a tree and act like a nut. +Born free, taxed to death. +My favorite part of a marathon is watching the reaction of runners who grab my plastic cup of vodka. +I heard a great joke about amnesia but I forgot it. +I was at an ATM an old lady asked me to help check her balance, so I pushed her over. +Whenever I find the key to success, someone changes the lock. +With great power, comes great electricity bills. +A bartender is just a pharmacist with a limited inventory. +Lottery: a tax on people who are bad at math. @@ -0,0 +1,92 @@ +import time +from collections import deque +import net.mapserv as mapserv +import badge +from loggers import debuglog +from utils import extends +from textutils import preprocess as pp +from textutils import (simplify_links, remove_formatting, + replace_emotes) +pp_actions = (simplify_links, remove_formatting, replace_emotes) + +sent_whispers = deque() + +afk_message = '*AFK* I am away from keyboard' +afk_ts = 0 +chat_bots = ["guild", "_IRC_"] + + +def send_whisper(nick, message): + badge.is_afk = False + ts = time.time() + sent_whispers.append((nick, message, ts)) + mapserv.cmsg_chat_whisper(nick, message) + + +def general_chat(message): + badge.is_afk = False + mapserv.cmsg_chat_message(message) + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + now = time.time() + while True: + try: + nick, msg, ts = sent_whispers.popleft() + except IndexError: + return + if now - ts < 1.0: + break + + if data.code == 0: + m = "[-> {}] {}".format(nick, pp(msg, pp_actions)) + debuglog.info(m) + else: + debuglog.warning("[error] {} is offline.".format(nick)) + + +@extends('smsg_being_chat') +def being_chat(data): + message = pp(data.message, pp_actions) + debuglog.info(message) + + +@extends('smsg_player_chat') +def player_chat(data): + message = pp(data.message, pp_actions) + debuglog.info(message) + + +@extends('smsg_whisper') +def got_whisper(data): + nick, message = data.nick, data.message + message = pp(message, pp_actions) + m = "[{} ->] {}".format(nick, message) + debuglog.info(m) + + if badge.is_afk: + if nick in chat_bots: + return + if message.startswith('!'): + return + now = time.time() + global afk_ts + if now > afk_ts + 20: + afk_ts = now + send_whisper(nick, afk_message) + badge.is_afk = True + + +@extends('smsg_party_chat') +def party_chat(data): + nick = mapserv.party_members.get(data.id, str(data.id)) + message = pp(data.message, pp_actions) + m = "[Party] {} : {}".format(nick, message) + debuglog.info(m) + + +@extends('smsg_gm_chat') +def gm_chat(data): + m = "[GM] {}".format(data.message) + debuglog.info(m) diff --git a/chat.pyc b/chat.pyc Binary files differnew file mode 100644 index 0000000..e23ff32 --- /dev/null +++ b/chat.pyc diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..89aeff2 --- /dev/null +++ b/commands.py @@ -0,0 +1,347 @@ + +import net.mapserv as mapserv +from net.inventory import get_item_index +from net.common import distance +import itemdb +from utils import preprocess_argument +from textutils import expand_links +from loggers import debuglog +import walkto +from actor import find_nearest_being +import status +import chat +import badge + +__all__ = [ 'commands', 'must_have_arg', + 'parse_player_name', 'process_line' ] + + +def must_have_arg(func): + + def wrapper(cmd, arg): + if len(arg) > 0: + return func(cmd, arg) + + wrapper.__doc__ = func.__doc__ + return wrapper + + +@preprocess_argument(expand_links) +def general_chat(msg): + '''Send message to #General chat''' + chat.general_chat(msg) + + +@must_have_arg +def send_whisper(_, arg): + '''Send whisper to player +/w "Player Name" Message... +/w NameWithourSpaces Message''' + nick, message = parse_player_name(arg) + if len(nick) > 0 and len(message) > 0: + chat.send_whisper(nick, message) + + +@must_have_arg +def send_party_message(_, msg): + '''Sent message to party''' + mapserv.cmsg_party_message(msg) + + +@must_have_arg +def set_direction(_, dir_str): + '''/turn down|up|left|right''' + d = {"down": 1, "left": 2, "up": 4, "right": 8} + dir_num = d.get(dir_str.lower(), -1) + if dir_num > 0: + mapserv.cmsg_player_change_dir(dir_num) + + +def sit_or_stand(cmd, _): + '''Use /sit or /stand for corresponding action''' + a = {"sit": 2, "stand": 3} + try: + action = a[cmd] + mapserv.cmsg_player_change_act(0, action) + except KeyError: + pass + + +@must_have_arg +def set_destination(_, xy): + '''/goto x y -- walk to given coordinates''' + try: + x, y = map(int, xy.split()) + mapserv.cmsg_player_change_dest(x, y) + except ValueError: + pass + + +@must_have_arg +def show_emote(_, emote): + '''Smile! +/emote ID''' + try: + mapserv.cmsg_player_emote(int(emote)) + except ValueError: + pass + + +@must_have_arg +def attack(_, arg): + '''Attack being (ID or Name)''' + try: + target = mapserv.beings_cache[int(arg)] + except (ValueError, KeyError): + target = find_nearest_being(name=arg, + ignored_ids=walkto.unreachable_ids) + + if target is not None: + walkto.walkto_and_action(target, 'attack') + else: + debuglog.warning("Being %s not found", arg) + + +@must_have_arg +def me_action(_, arg): + '''You can guess :-)''' + general_chat("*{}*".format(arg)) + + +@must_have_arg +def item_action(cmd, name_or_id): + '''/use <item> +/equip <item> +/unequip <item> +item can be either item name or ID''' + item_id = -10 + item_db = itemdb.item_names + + try: + item_id = int(name_or_id) + except ValueError: + for id_, name in item_db.iteritems(): + if name == name_or_id: + item_id = id_ + + if item_id < 0: + debuglog.warning("Unknown item: %s", name_or_id) + return + + index = get_item_index(item_id) + if index > 0: + if cmd == 'use': + mapserv.cmsg_player_inventory_use(index, item_id) + elif cmd == 'equip': + mapserv.cmsg_player_equip(index) + elif cmd == 'unequip': + mapserv.cmsg_player_unequip(index) + else: + debuglog.error("You don't have %s", name_or_id) + + +@must_have_arg +def drop_item(_, arg): + '''/drop <amount> <name or id>''' + s = arg.split(None, 1) + + try: + amount = int(s[0]) + except ValueError: + debuglog.warning('Usage: /drop <amount> <name or id>') + return + + item_id = -10 + item_db = itemdb.item_names + + try: + item_id = int(s[1]) + except ValueError: + for id_, name in item_db.iteritems(): + if name == s[1]: + item_id = id_ + + if item_id < 0: + debuglog.warning("Unknown item: %s", s[1]) + return + + index = get_item_index(item_id) + if index > 0: + mapserv.cmsg_player_inventory_drop(index, amount) + else: + debuglog.error("You don't have %s", s[1]) + + +def show_inventory(*unused): + '''Show inventory''' + inv = {} + for itemId, amount in mapserv.player_inventory.values(): + inv[itemId] = inv.setdefault(itemId, 0) + amount + + s = [] + for itemId, amount in inv.items(): + if amount > 1: + s.append('{} [{}]'.format(amount, itemdb.item_name(itemId))) + else: + s.append('[{}]'.format(itemdb.item_name(itemId))) + + debuglog.info(', '.join(s)) + + +def show_zeny(*unused): + '''Show player money''' + debuglog.info('You have {} GP'.format(mapserv.player_money)) + + +def print_beings(cmd, btype): + '''Show nearby beings +/beings -- show all beings +/beings player|npc|portal|monster --show only given being type''' + for l in status.nearby(btype): + debuglog.info(l) + + +def player_position(*unused): + '''Show player position''' + debuglog.info(status.player_position()) + + +def respawn(*unused): + '''Respawn''' + mapserv.cmsg_player_respawn() + + +def pickup(*unused): + '''Pickup nearby item, if any''' + px = mapserv.player_pos['x'] + py = mapserv.player_pos['y'] + for item in mapserv.floor_items.values(): + if distance(px, py, item.x, item.y) < 2: + mapserv.cmsg_item_pickup(item.id) + + +def show_status(_, arg): + '''Show various stats''' + if arg: + all_stats = arg.split() + else: + all_stats = ('stats', 'hpmp', 'weight', 'points', + 'zeny', 'attack', 'skills') + + sr = status.stats_repr(*all_stats) + debuglog.info(' | '.join(sr.values())) + + +def cmd_afk(_, arg): + '''Become AFK''' + if arg: + chat.afk_message = '*AFK* ' + arg + badge.is_afk = True + debuglog.info(chat.afk_message) + + +def cmd_back(*unused): + '''Disable AFK''' + badge.is_afk = False + + +def print_help(_, hcmd): + '''Show help +/help -- show all commands +/help CMD -- show help on CMD''' + s = ' '.join(commands.keys()) + if hcmd in commands: + docstring = commands[hcmd].__doc__ + if docstring: + debuglog.info(docstring) + else: + debuglog.info('No help available for command /{}'.format(hcmd)) + else: + debuglog.info("[help] commands: %s", s) + + +def cmd_exec(_, arg): + try: + exec arg + except Exception, e: + debuglog.error(e.message) + + +def command_not_found(cmd): + debuglog.warning("[warning] command not found: %s. Try /help.", cmd) + + +def parse_player_name(line): + line = line.lstrip() + if len(line) < 2: + return "", "" + if line[0] == '"': + end = line[1:].find('"') + if end < 0: + return line[1:], "" + else: + return line[1:end + 1], line[end + 3:] + else: + end = line.find(" ") + if end < 0: + return line, "" + else: + return line[:end], line[end + 1:] + + +commands = { + "w" : send_whisper, + "whisper" : send_whisper, + "p" : send_party_message, + "party" : send_party_message, + "e" : show_emote, + "emote" : show_emote, + "dir" : set_direction, + "direction" : set_direction, + "turn" : set_direction, + "sit" : sit_or_stand, + "stand" : sit_or_stand, + "goto" : set_destination, + "nav" : set_destination, + "dest" : set_destination, + "me" : me_action, + "use" : item_action, + "equip" : item_action, + "unequip" : item_action, + "attack" : attack, + "beings" : print_beings, + "inv" : show_inventory, + "zeny" : show_zeny, + "where" : player_position, + "respawn" : respawn, + "pickup" : pickup, + "drop" : drop_item, + "status" : show_status, + "afk" : cmd_afk, + "back" : cmd_back, + "help" : print_help, + "exec" : cmd_exec, +} + + +def process_line(line): + if line == "": + return + + elif line[0] == "/": + end = line.find(" ") + if end < 0: + cmd = line[1:] + arg = "" + else: + cmd = line[1:end] + arg = line[end + 1:] + + if cmd in commands: + func = commands[cmd] + func(cmd, arg) + else: + command_not_found(cmd) + + else: + general_chat(line) diff --git a/commands.pyc b/commands.pyc Binary files differnew file mode 100644 index 0000000..161f48c --- /dev/null +++ b/commands.pyc diff --git a/curses/cui.py b/curses/cui.py new file mode 100644 index 0000000..5405765 --- /dev/null +++ b/curses/cui.py @@ -0,0 +1,78 @@ +#-*- coding: utf-8 -*- +""" +Curses-based console user interface for TMW chat client. +""" + +import curses +from curses.textpad import Textbox + +stdscr = None +chatlog_win = None +input_win = None +players_win = None +input_textbox = None + + +def init(): + global stdscr, chatlog_win, input_win, players_win, input_textbox + + stdscr = curses.initscr() + curses.cbreak() + curses.noecho() + stdscr.keypad(1) + + h, w = stdscr.getmaxyx() + PNW = 20 # player name width + INH = 4 # input window height + + stdscr.vline(0, w - PNW - 1, curses.ACS_VLINE, h) + stdscr.hline(h - INH - 1, 0, curses.ACS_HLINE, w - PNW - 1) + + chatlog_win = curses.newwin(h - INH - 1, w - PNW - 1, 0, 0) + input_win = curses.newwin(INH, w - PNW - 1, h - INH, 0) + players_win = curses.newwin(h, PNW, 0, w - PNW) + + chatlog_win.idlok(1) + chatlog_win.scrollok(1) + + players_win.idlok(1) + players_win.scrollok(1) + + input_textbox = Textbox(input_win) + input_textbox.stripspaces = True + + stdscr.noutrefresh() + input_win.noutrefresh() + players_win.noutrefresh() + chatlog_win.noutrefresh() + + curses.doupdate() + + +def chatlog_append(line): + if line[-1] != "\n": + line = line + "\n" + chatlog_win.addstr(line) + chatlog_win.refresh() + + +def input_loop(callback): + def v(ch): + # chatlog_append(curses.keyname(ch)) + if ch in (curses.KEY_ENTER, curses.ascii.NL): + return curses.ascii.BEL + return ch + + cmd = '' + while cmd not in ('/exit', '/quit'): + cmd = input_textbox.edit(v).strip() + callback(cmd) + input_win.clear() + input_win.move(0, 0) + + +def finalize(): + stdscr.keypad(0) + curses.echo() + curses.nocbreak() + curses.endwin() diff --git a/curses/cui.pyc b/curses/cui.pyc Binary files differnew file mode 100644 index 0000000..2a46ef6 --- /dev/null +++ b/curses/cui.pyc diff --git a/curses/handlers.py b/curses/handlers.py new file mode 100644 index 0000000..28a7562 --- /dev/null +++ b/curses/handlers.py @@ -0,0 +1,39 @@ + +import net.mapserv as mapserv +from utils import extends +import cui +import textutils +from loggers import debuglog + + +__all__ = [] + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + if data.code == 0: + last_nick = mapserv.last_whisper['to'] + cui.input_win.clear() + cui.input_win.addstr('/w "{}" '.format(last_nick)) + cui.input_win.refresh() + + +@extends('smsg_player_warp') +def player_warp(data): + mapserv.cmsg_map_loaded() + m = "[warp] {} ({},{})".format(data.map, data.x, data.y) + debuglog.info(m) + + +@extends('smsg_map_login_success') +def map_login_success(data): + mapserv.cmsg_map_loaded() + + +@extends('smsg_connection_problem') +def connection_problem(data): + error_codes = { + 2 : "Account already in use" + } + msg = error_codes.get(data.code, str(data.code)) + debuglog.error('Connection problem: %s', msg) diff --git a/curses/handlers.pyc b/curses/handlers.pyc Binary files differnew file mode 100644 index 0000000..e75a37f --- /dev/null +++ b/curses/handlers.pyc diff --git a/curses/tmwcli.py b/curses/tmwcli.py new file mode 100644 index 0000000..679019b --- /dev/null +++ b/curses/tmwcli.py @@ -0,0 +1,134 @@ +#!/usr/bin/python2 + +import os +import sys +import logging +import time +import asyncore +import threading +from ConfigParser import ConfigParser + +# add .. to PYTHONPATH +parent, _ = os.path.split(os.getcwd()) +sys.path.insert(0, parent) +sys.path.insert(1, os.path.join(parent, "plugins")) + +try: + import construct + del construct +except ImportError: + sys.path.insert(1, os.path.join(parent, "external")) + +del parent + +import cui +import handlers +import net +import net.mapserv as mapserv +import plugins +import monsterdb +import itemdb +from commands import process_line +from net.onlineusers import OnlineUsers +from loggers import netlog, debuglog +from logicmanager import logic_manager + + +class SideBarUpdater(threading.Thread): + + def __init__(self, window, online_users_obj, update_interval=20): + self._active = True + self._timer = 0 + self._update_interval = update_interval + self._online_users_obj = online_users_obj + self._window = window + threading.Thread.__init__(self) + + def run(self): + while self._active: + if (time.time() - self._timer) > self._update_interval: + self._window.clear() + for user in self._online_users_obj.online_users: + print user + self._window.addstr(user + '\n') + self._window.refresh() + self._timer = time.time() + else: + time.sleep(1.0) + + def stop(self): + self._active = False + + +class CursesDebugLogHandler(logging.Handler): + def emit(self, record): + msg = self.format(record) + cui.chatlog_append(msg) + + +def loop(): + try: + while True: + asyncore.loop(timeout=0.2, count=5) + logic_manager.tick() + except KeyboardInterrupt: + return + + +if __name__ == "__main__": + config = ConfigParser() + if len(sys.argv) > 1: + config.read(sys.argv[1]) + else: + config.read('../manachat.ini') + + rootLogger = logging.getLogger('') + rootLogger.addHandler(logging.NullHandler()) + + dbgh = CursesDebugLogHandler() + dbgh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%H:%M")) + debuglog.addHandler(dbgh) + debuglog.setLevel(logging.INFO) + + if config.getboolean('Other', 'log_network_packets'): + import os + import tempfile + + logfile = os.path.join(tempfile.gettempdir(), "netlog.txt") + netlog.setLevel(logging.INFO) + fh = logging.FileHandler(logfile, mode="w") + fmt = logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S") + fh.setFormatter(fmt) + netlog.addHandler(fh) + + cui.init() + + plugins.load_plugins(config) + + monsterdb.read_monster_db('../monsterdb.txt') + itemdb.load_itemdb('../itemdb.txt') + + online_users = OnlineUsers(config.get('Other', 'online_txt_url')) + online_users.start() + side_bar_updater = SideBarUpdater(cui.players_win, online_users) + side_bar_updater.start() + + net.login(host=config.get('Server', 'host'), + port=config.getint('Server', 'port'), + username=config.get('Player', 'username'), + password=config.get('Player', 'password'), + charname=config.get('Player', 'charname')) + + t = threading.Thread(target=loop) + t.setDaemon(True) + t.start() + + cui.input_loop(process_line) + + side_bar_updater.stop() + online_users.stop() + cui.finalize() + + mapserv.cleanup() diff --git a/external/LINCENCE.six b/external/LINCENCE.six new file mode 100644 index 0000000..e558f9d --- /dev/null +++ b/external/LINCENCE.six @@ -0,0 +1,18 @@ +Copyright (c) 2010-2015 Benjamin Peterson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/external/README.six b/external/README.six new file mode 100644 index 0000000..ee628a9 --- /dev/null +++ b/external/README.six @@ -0,0 +1,16 @@ +Six is a Python 2 and 3 compatibility library. It provides utility functions +for smoothing over the differences between the Python versions with the goal of +writing Python code that is compatible on both Python versions. See the +documentation for more information on what is provided. + +Six supports every Python version since 2.6. It is contained in only one Python +file, so it can be easily copied into your project. (The copyright and license +notice must be retained.) + +Online documentation is at https://pythonhosted.org/six/. + +Bugs can be reported to https://bitbucket.org/gutworth/six. The code can also +be found there. + +For questions about six or porting in general, email the python-porting mailing +list: https://mail.python.org/mailman/listinfo/python-porting diff --git a/external/construct/LICENSE b/external/construct/LICENSE new file mode 100644 index 0000000..a3c7898 --- /dev/null +++ b/external/construct/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2006-2013 + Tomer Filiba (tomerfiliba@gmail.com) + Corbin Simpson (MostAwesomeDude@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/construct/README.rst b/external/construct/README.rst new file mode 100644 index 0000000..207bd82 --- /dev/null +++ b/external/construct/README.rst @@ -0,0 +1,79 @@ +Construct2 +========== +Construct is a powerful **declarative** parser (and builder) for binary data. + +Instead of writing *imperative code* to parse a piece of data, you declaratively +define a *data structure* that describes your data. As this data structure is not +code, you can use it in one direction to *parse* data into Pythonic objects, +and in the other direction, convert ("build") objects into binary data. + +The library provides both simple, atomic constructs (such as integers of various sizes), +as well as composite ones which allow you form hierarchical structures of increasing complexity. +Construct features **bit and byte granularity**, easy debugging and testing, an +**easy-to-extend subclass system**, and lots of primitive constructs to make your +work easier: + +* Fields: raw bytes or numerical types +* Structs and Sequences: combine simpler constructs into more complex ones +* Adapters: change how data is represented +* Arrays/Ranges: duplicate constructs +* Meta-constructs: use the context (history) to compute the size of data +* If/Switch: branch the computational path based on the context +* On-demand (lazy) parsing: read only what you require +* Pointers: jump from here to there in the data stream + +.. note:: + `Construct3 <http://tomerfiliba.com/blog/Survey-of-Construct3/>`_ is a rewrite of Construct2; + the two are incompatible, thus ``construct3`` will be released as a *different package*. + Construct 2.5 is the last release of the 2.x codebase. + + Construct 2.5 drops the experimental text parsing support that was added in Construct 2.0; + it was highly inefficient and I chose to concentrate on binary data. + +Example +------- + +A ``PascalString`` is a string prefixed by its length:: + + >>> from construct import * + >>> + >>> PascalString = Struct("PascalString", + ... UBInt8("length"), + ... Bytes("data", lambda ctx: ctx.length), + ... ) + >>> + >>> PascalString.parse("\x05helloXXX") + Container({'length': 5, 'data': 'hello'}) + >>> PascalString.build(Container(length = 6, data = "foobar")) + '\x06foobar' + +Instead of specifying the length manually, let's use an adapter:: + + >>> PascalString2 = ExprAdapter(PascalString, + ... encoder = lambda obj, ctx: Container(length = len(obj), data = obj), + ... decoder = lambda obj, ctx: obj.data + ... ) + >>> PascalString2.parse("\x05hello") + 'hello' + >>> PascalString2.build("i'm a long string") + "\x11i'm a long string" + +See more examples of `file formats <https://github.com/construct/construct/tree/master/construct/formats>`_ +and `network protocols <https://github.com/construct/construct/tree/master/construct/protocols>`_ +in the repository. + +Resources +--------- +Construct's homepage is `<http://construct.readthedocs.org>`_, where you can find all kinds +of docs and resources. The library itself is developed on `github <https://github.com/construct/construct>`_; +please use `github issues <https://github.com/construct/construct/issues>`_ to report bugs, and +github pull-requests to send in patches. For general discussion or questions, please use the +`new discussion group <https://groups.google.com/d/forum/construct3>`_. + +Requirements +------------ +Construct should run on any Python 2.5-3.3 implementation. + +Its only requirement is `six <http://pypi.python.org/pypi/six>`_, which is used to overcome the +differences between Python 2 and 3. + diff --git a/external/construct/__init__.py b/external/construct/__init__.py new file mode 100644 index 0000000..024fe33 --- /dev/null +++ b/external/construct/__init__.py @@ -0,0 +1,79 @@ +""" +Construct 2 -- Parsing Made Fun + +Homepage: + http://construct.readthedocs.org + +Hands-on example: + >>> from construct import * + >>> s = Struct("foo", + ... UBInt8("a"), + ... UBInt16("b"), + ... ) + >>> print s.parse(b"\\x01\\x02\\x03") + Container: + a = 1 + b = 515 + >>> s.build(Container(a = 1, b = 0x0203)) + b"\\x01\\x02\\x03" +""" + +from construct.core import (AdaptationError, Adapter, Anchor, ArrayError, Buffered, Construct, ConstructError, + Container, FieldError, FormatField, LazyBound, LazyContainer, ListContainer, MetaArray, MetaField, OnDemand, + OverwriteError, Packer, Pass, Peek, Pointer, Range, RangeError, Reconfig, RepeatUntil, Restream, Select, + SelectError, Sequence, SizeofError, StaticField, Struct, Subconstruct, Switch, SwitchError, Terminator, + TerminatorError, ULInt24, Union, Value) +from construct.adapters import (BitIntegerAdapter, BitIntegerError, CStringAdapter, ConstAdapter, ConstError, + ExprAdapter, FlagsAdapter, FlagsContainer, HexDumpAdapter, HexString, IndexingAdapter, LengthValueAdapter, + MappingAdapter, MappingError, NoneOf, OneOf, PaddedStringAdapter, PaddingAdapter, PaddingError, SlicingAdapter, + StringAdapter, TunnelAdapter, ValidationError, Validator) +from construct.macros import (Alias, Aligned, AlignedStruct, Array, BFloat32, BFloat64, Bit, BitField, + BitStreamReader, BitStreamWriter, BitStruct, Bitwise, CString, Embedded, EmbeddedBitStruct, Enum, Field, + Flag, FlagsEnum, GreedyRange, If, IfThenElse, LFloat32, LFloat64, Magic, NFloat32, NFloat64, Nibble, Octet, + OnDemandPointer, OpenRange, Optional, OptionalGreedyRange, Padding, PascalString, PrefixedArray, + Rename, SBInt16, SBInt32, SBInt64, SBInt8, SLInt16, SLInt32, SLInt64, SLInt8, SNInt16, SNInt32, SNInt64, + SNInt8, SeqOfOne, String, SymmetricMapping, UBInt16, UBInt32, UBInt64, UBInt8, ULInt16, ULInt32, ULInt64, + ULInt8, UNInt16, UNInt32, UNInt64, UNInt8) +from construct.lib.expr import this +from construct.debug import Probe, Debugger +from construct.version import version, version_string as __version__ + + +#=============================================================================== +# Metadata +#=============================================================================== +__author__ = "Tomer Filiba <tomerfiliba@gmail.com>, Corbin Simpson <MostAwesomeDude@gmail.com>" + +#=============================================================================== +# Shorthand expressions +#=============================================================================== +Bits = BitField +Byte = UBInt8 +Bytes = Field +Const = ConstAdapter +Tunnel = TunnelAdapter +Embed = Embedded + +#=============================================================================== +# exposed names +#=============================================================================== +__all__ = [ + 'AdaptationError', 'Adapter', 'Alias', 'Aligned', 'AlignedStruct', 'Anchor', 'Array', 'ArrayError', + 'BFloat32', 'BFloat64', 'Bit', 'BitField', 'BitIntegerAdapter', 'BitIntegerError', 'BitStreamReader', + 'BitStreamWriter', 'BitStruct', 'Bitwise', 'Buffered', 'CString', 'CStringAdapter', 'ConstAdapter', + 'ConstError', 'Construct', 'ConstructError', 'Container', 'Debugger', 'Embedded', 'EmbeddedBitStruct', + 'Enum', 'ExprAdapter', 'Field', 'FieldError', 'Flag', 'FlagsAdapter', 'FlagsContainer', 'FlagsEnum', + 'FormatField', 'GreedyRange', 'HexDumpAdapter', 'HexString', 'If', 'IfThenElse', 'IndexingAdapter', + 'LFloat32', 'LFloat64', 'LazyBound', 'LazyContainer', 'LengthValueAdapter', 'ListContainer', 'Magic', + 'MappingAdapter', 'MappingError', 'MetaArray', 'MetaField', 'NFloat32', 'NFloat64', 'Nibble', 'NoneOf', + 'Octet', 'OnDemand', 'OnDemandPointer', 'OneOf', 'OpenRange', 'Optional', 'OptionalGreedyRange', + 'OverwriteError', 'Packer', 'PaddedStringAdapter', 'Padding', 'PaddingAdapter', 'PaddingError', + 'PascalString', 'Pass', 'Peek', 'Pointer', 'PrefixedArray', 'Probe', 'Range', 'RangeError', 'Reconfig', + 'Rename', 'RepeatUntil', 'Restream', 'SBInt16', 'SBInt32', 'SBInt64', 'SBInt8', 'SLInt16', 'SLInt32', + 'SLInt64', 'SLInt8', 'SNInt16', 'SNInt32', 'SNInt64', 'SNInt8', 'Select', 'SelectError', 'SeqOfOne', + 'Sequence', 'SizeofError', 'SlicingAdapter', 'StaticField', 'String', 'StringAdapter', 'Struct', + 'Subconstruct', 'Switch', 'SwitchError', 'SymmetricMapping', 'Terminator', 'TerminatorError', + 'TunnelAdapter', 'UBInt16', 'UBInt32', 'UBInt64', 'UBInt8', 'ULInt16', 'ULInt24', 'ULInt32', 'ULInt64', + 'ULInt8', 'UNInt16', 'UNInt32', 'UNInt64', 'UNInt8', 'Union', 'ValidationError', 'Validator', 'Value', + 'this', 'Bits', 'Byte', 'Bytes', 'Const', 'Tunnel', 'Embed', +] diff --git a/external/construct/__init__.pyc b/external/construct/__init__.pyc Binary files differnew file mode 100644 index 0000000..2bdb820 --- /dev/null +++ b/external/construct/__init__.pyc diff --git a/external/construct/adapters.py b/external/construct/adapters.py new file mode 100644 index 0000000..b6a85df --- /dev/null +++ b/external/construct/adapters.py @@ -0,0 +1,470 @@ +from construct.core import Adapter, AdaptationError, Pass +from construct.lib import int_to_bin, bin_to_int, swap_bytes +from construct.lib import FlagsContainer, HexString +from six import BytesIO +import six + + +try: + bytes +except NameError: + bytes = str + +#=============================================================================== +# exceptions +#=============================================================================== +class BitIntegerError(AdaptationError): + pass +class MappingError(AdaptationError): + pass +class ConstError(AdaptationError): + pass +class ValidationError(AdaptationError): + pass +class PaddingError(AdaptationError): + pass + +#=============================================================================== +# adapters +#=============================================================================== +class BitIntegerAdapter(Adapter): + """ + Adapter for bit-integers (converts bitstrings to integers, and vice versa). + See BitField. + + :param subcon: the subcon to adapt + :param width: the size of the subcon, in bits + :param swapped: whether to swap byte order (little endian/big endian). + default is False (big endian) + :param signed: whether the value is signed (two's complement). the default + is False (unsigned) + :param bytesize: number of bits per byte, used for byte-swapping (if swapped). + default is 8. + """ + __slots__ = ["width", "swapped", "signed", "bytesize"] + def __init__(self, subcon, width, swapped = False, signed = False, + bytesize = 8): + Adapter.__init__(self, subcon) + self.width = width + self.swapped = swapped + self.signed = signed + self.bytesize = bytesize + def _encode(self, obj, context): + if obj < 0 and not self.signed: + raise BitIntegerError("object is negative, but field is not signed", + obj) + obj2 = int_to_bin(obj, width = self.width(context) if callable(self.width) else self.width) + if self.swapped: + obj2 = swap_bytes(obj2, bytesize = self.bytesize) + return obj2 + def _decode(self, obj, context): + if self.swapped: + obj = swap_bytes(obj, bytesize = self.bytesize) + return bin_to_int(obj, signed = self.signed) + +class MappingAdapter(Adapter): + """ + Adapter that maps objects to other objects. + See SymmetricMapping and Enum. + + :param subcon: the subcon to map + :param decoding: the decoding (parsing) mapping (a dict) + :param encoding: the encoding (building) mapping (a dict) + :param decdefault: the default return value when the object is not found + in the decoding mapping. if no object is given, an exception is raised. + if ``Pass`` is used, the unmapped object will be passed as-is + :param encdefault: the default return value when the object is not found + in the encoding mapping. if no object is given, an exception is raised. + if ``Pass`` is used, the unmapped object will be passed as-is + """ + __slots__ = ["encoding", "decoding", "encdefault", "decdefault"] + def __init__(self, subcon, decoding, encoding, + decdefault = NotImplemented, encdefault = NotImplemented): + Adapter.__init__(self, subcon) + self.decoding = decoding + self.encoding = encoding + self.decdefault = decdefault + self.encdefault = encdefault + def _encode(self, obj, context): + try: + return self.encoding[obj] + except (KeyError, TypeError): + if self.encdefault is NotImplemented: + raise MappingError("no encoding mapping for %r [%s]" % ( + obj, self.subcon.name)) + if self.encdefault is Pass: + return obj + return self.encdefault + def _decode(self, obj, context): + try: + return self.decoding[obj] + except (KeyError, TypeError): + if self.decdefault is NotImplemented: + raise MappingError("no decoding mapping for %r [%s]" % ( + obj, self.subcon.name)) + if self.decdefault is Pass: + return obj + return self.decdefault + +class FlagsAdapter(Adapter): + """ + Adapter for flag fields. Each flag is extracted from the number, resulting + in a FlagsContainer object. Not intended for direct usage. See FlagsEnum. + + :param subcon: the subcon to extract + :param flags: a dictionary mapping flag-names to their value + """ + __slots__ = ["flags"] + def __init__(self, subcon, flags): + Adapter.__init__(self, subcon) + self.flags = flags + def _encode(self, obj, context): + flags = 0 + for name, value in self.flags.items(): + if getattr(obj, name, False): + flags |= value + return flags + def _decode(self, obj, context): + obj2 = FlagsContainer() + for name, value in self.flags.items(): + setattr(obj2, name, bool(obj & value)) + return obj2 + +class StringAdapter(Adapter): + """ + Adapter for strings. Converts a sequence of characters into a python + string, and optionally handles character encoding. See String. + + :param subcon: the subcon to convert + :param encoding: the character encoding name (e.g., "utf8"), or None to + return raw bytes (usually 8-bit ASCII). + """ + __slots__ = ["encoding"] + def __init__(self, subcon, encoding = None): + Adapter.__init__(self, subcon) + self.encoding = encoding + def _encode(self, obj, context): + if self.encoding: + if isinstance(self.encoding, str): + obj = obj.encode(self.encoding) + else: + obj = self.encoding.encode(obj) + return obj + def _decode(self, obj, context): + if not isinstance(obj, bytes): + obj = six.b("").join(obj) + if self.encoding: + if isinstance(self.encoding, str): + obj = obj.decode(self.encoding) + else: + obj = self.encoding.decode(obj) + return obj + +class PaddedStringAdapter(Adapter): + r""" + Adapter for padded strings. See String. + + :param subcon: the subcon to adapt + :param padchar: the padding character. default is "\x00". + :param paddir: the direction where padding is placed ("right", "left", or + "center"). the default is "right". + :param trimdir: the direction where trimming will take place ("right" or + "left"). the default is "right". trimming is only meaningful for + building, when the given string is too long. + """ + __slots__ = ["padchar", "paddir", "trimdir"] + def __init__(self, subcon, padchar = six.b("\x00"), paddir = "right", trimdir = "right"): + if paddir not in ("right", "left", "center"): + raise ValueError("paddir must be 'right', 'left' or 'center'", paddir) + if trimdir not in ("right", "left"): + raise ValueError("trimdir must be 'right' or 'left'", trimdir) + Adapter.__init__(self, subcon) + self.padchar = padchar + self.paddir = paddir + self.trimdir = trimdir + def _decode(self, obj, context): + if self.paddir == "right": + obj = obj.rstrip(self.padchar) + elif self.paddir == "left": + obj = obj.lstrip(self.padchar) + else: + obj = obj.strip(self.padchar) + return obj + def _encode(self, obj, context): + size = self._sizeof(context) + if self.paddir == "right": + obj = obj.ljust(size, self.padchar) + elif self.paddir == "left": + obj = obj.rjust(size, self.padchar) + else: + obj = obj.center(size, self.padchar) + if len(obj) > size: + if self.trimdir == "right": + obj = obj[:size] + else: + obj = obj[-size:] + return obj + +class LengthValueAdapter(Adapter): + """ + Adapter for length-value pairs. It extracts only the value from the + pair, and calculates the length based on the value. + See PrefixedArray and PascalString. + + :param subcon: the subcon returning a length-value pair + """ + __slots__ = [] + def _encode(self, obj, context): + return (len(obj), obj) + def _decode(self, obj, context): + return obj[1] + +class CStringAdapter(StringAdapter): + r""" + Adapter for C-style strings (strings terminated by a terminator char). + + :param subcon: the subcon to convert + :param terminators: a sequence of terminator chars. default is "\x00". + :param encoding: the character encoding to use (e.g., "utf8"), or None to return raw-bytes. + the terminator characters are not affected by the encoding. + """ + __slots__ = ["terminators"] + def __init__(self, subcon, terminators = six.b("\x00"), encoding = None): + StringAdapter.__init__(self, subcon, encoding = encoding) + self.terminators = terminators + def _encode(self, obj, context): + return StringAdapter._encode(self, obj, context) + self.terminators[0:1] + def _decode(self, obj, context): + return StringAdapter._decode(self, six.b('').join(obj[:-1]), context) + +class TunnelAdapter(Adapter): + """ + Adapter for tunneling (as in protocol tunneling). A tunnel is construct + nested upon another (layering). For parsing, the lower layer first parses + the data (note: it must return a string!), then the upper layer is called + to parse that data (bottom-up). For building it works in a top-down manner; + first the upper layer builds the data, then the lower layer takes it and + writes it to the stream. + + :param subcon: the lower layer subcon + :param inner_subcon: the upper layer (tunneled/nested) subcon + + Example:: + + # a pascal string containing compressed data (zlib encoding), so first + # the string is read, decompressed, and finally re-parsed as an array + # of UBInt16 + TunnelAdapter( + PascalString("data", encoding = "zlib"), + GreedyRange(UBInt16("elements")) + ) + + """ + __slots__ = ["inner_subcon"] + def __init__(self, subcon, inner_subcon): + Adapter.__init__(self, subcon) + self.inner_subcon = inner_subcon + def _decode(self, obj, context): + return self.inner_subcon._parse(BytesIO(obj), context) + def _encode(self, obj, context): + stream = BytesIO() + self.inner_subcon._build(obj, stream, context) + return stream.getvalue() + +class ExprAdapter(Adapter): + """ + A generic adapter that accepts 'encoder' and 'decoder' as parameters. You + can use ExprAdapter instead of writing a full-blown class when only a + simple expression is needed. + + :param subcon: the subcon to adapt + :param encoder: a function that takes (obj, context) and returns an encoded version of obj + :param decoder: a function that takes (obj, context) and returns an decoded version of obj + + Example:: + + ExprAdapter(UBInt8("foo"), + encoder = lambda obj, ctx: obj / 4, + decoder = lambda obj, ctx: obj * 4, + ) + """ + __slots__ = ["_encode", "_decode"] + def __init__(self, subcon, encoder, decoder): + Adapter.__init__(self, subcon) + self._encode = encoder + self._decode = decoder + +class HexDumpAdapter(Adapter): + """ + Adapter for hex-dumping strings. It returns a HexString, which is a string + """ + __slots__ = ["linesize"] + def __init__(self, subcon, linesize = 16): + Adapter.__init__(self, subcon) + self.linesize = linesize + def _encode(self, obj, context): + return obj + def _decode(self, obj, context): + return HexString(obj, linesize = self.linesize) + +class ConstAdapter(Adapter): + """ + Adapter for enforcing a constant value ("magic numbers"). When decoding, + the return value is checked; when building, the value is substituted in. + + :param subcon: the subcon to validate + :param value: the expected value + + Example:: + + Const(Field("signature", 2), "MZ") + """ + __slots__ = ["value"] + def __init__(self, subcon, value): + Adapter.__init__(self, subcon) + self.value = value + def _encode(self, obj, context): + if obj is None or obj == self.value: + return self.value + else: + raise ConstError("expected %r, found %r" % (self.value, obj)) + def _decode(self, obj, context): + if obj != self.value: + raise ConstError("expected %r, found %r" % (self.value, obj)) + return obj + +class SlicingAdapter(Adapter): + """ + Adapter for slicing a list (getting a slice from that list) + + :param subcon: the subcon to slice + :param start: start index + :param stop: stop index (or None for up-to-end) + :param step: step (or None for every element) + """ + __slots__ = ["start", "stop", "step"] + def __init__(self, subcon, start, stop = None): + Adapter.__init__(self, subcon) + self.start = start + self.stop = stop + def _encode(self, obj, context): + if self.start is None: + return obj + return [None] * self.start + obj + def _decode(self, obj, context): + return obj[self.start:self.stop] + +class IndexingAdapter(Adapter): + """ + Adapter for indexing a list (getting a single item from that list) + + :param subcon: the subcon to index + :param index: the index of the list to get + """ + __slots__ = ["index"] + def __init__(self, subcon, index): + Adapter.__init__(self, subcon) + if type(index) is not int: + raise TypeError("index must be an integer", type(index)) + self.index = index + def _encode(self, obj, context): + return [None] * self.index + [obj] + def _decode(self, obj, context): + return obj[self.index] + +class PaddingAdapter(Adapter): + r""" + Adapter for padding. + + :param subcon: the subcon to pad + :param pattern: the padding pattern (character). default is "\x00" + :param strict: whether or not to verify, during parsing, that the given + padding matches the padding pattern. default is False (unstrict) + """ + __slots__ = ["pattern", "strict"] + def __init__(self, subcon, pattern = six.b("\x00"), strict = False): + Adapter.__init__(self, subcon) + self.pattern = pattern + self.strict = strict + def _encode(self, obj, context): + return self._sizeof(context) * self.pattern + def _decode(self, obj, context): + if self.strict: + expected = self._sizeof(context) * self.pattern + if obj != expected: + raise PaddingError("expected %r, found %r" % (expected, obj)) + return obj + + +#=============================================================================== +# validators +#=============================================================================== +class Validator(Adapter): + """ + Abstract class: validates a condition on the encoded/decoded object. + Override _validate(obj, context) in deriving classes. + + :param subcon: the subcon to validate + """ + __slots__ = [] + def _decode(self, obj, context): + if not self._validate(obj, context): + raise ValidationError("invalid object", obj) + return obj + def _encode(self, obj, context): + return self._decode(obj, context) + def _validate(self, obj, context): + raise NotImplementedError() + +class OneOf(Validator): + """ + Validates that the object is one of the listed values. + + :param subcon: object to validate + :param valids: a set of valid values + + Example:: + + >>> OneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x05") + 5 + >>> OneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x08") + Traceback (most recent call last): + ... + construct.core.ValidationError: ('invalid object', 8) + >>> + >>> OneOf(UBInt8("foo"), [4,5,6,7]).build(5) + '\\x05' + >>> OneOf(UBInt8("foo"), [4,5,6,7]).build(9) + Traceback (most recent call last): + ... + construct.core.ValidationError: ('invalid object', 9) + """ + __slots__ = ["valids"] + def __init__(self, subcon, valids): + Validator.__init__(self, subcon) + self.valids = valids + def _validate(self, obj, context): + return obj in self.valids + +class NoneOf(Validator): + """ + Validates that the object is none of the listed values. + + :param subcon: object to validate + :param invalids: a set of invalid values + + Example:: + + >>> NoneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x08") + 8 + >>> NoneOf(UBInt8("foo"), [4,5,6,7]).parse("\\x06") + Traceback (most recent call last): + ... + construct.core.ValidationError: ('invalid object', 6) + """ + __slots__ = ["invalids"] + def __init__(self, subcon, invalids): + Validator.__init__(self, subcon) + self.invalids = invalids + def _validate(self, obj, context): + return obj not in self.invalids diff --git a/external/construct/adapters.pyc b/external/construct/adapters.pyc Binary files differnew file mode 100644 index 0000000..b5a6c79 --- /dev/null +++ b/external/construct/adapters.pyc diff --git a/external/construct/core.py b/external/construct/core.py new file mode 100644 index 0000000..e1800e0 --- /dev/null +++ b/external/construct/core.py @@ -0,0 +1,1411 @@ +from struct import Struct as Packer + +from construct.lib.py3compat import BytesIO, advance_iterator, bchr +from construct.lib import Container, ListContainer, LazyContainer +import sys +import six + +try: + bytes +except NameError: + bytes = str + +#=============================================================================== +# exceptions +#=============================================================================== +class ConstructError(Exception): + pass +class FieldError(ConstructError): + pass +class SizeofError(ConstructError): + pass +class AdaptationError(ConstructError): + pass +class ArrayError(ConstructError): + pass +class RangeError(ConstructError): + pass +class SwitchError(ConstructError): + pass +class SelectError(ConstructError): + pass +class TerminatorError(ConstructError): + pass +class OverwriteError(ValueError): + pass + +#=============================================================================== +# abstract constructs +#=============================================================================== +class Construct(object): + """ + The mother of all constructs. + + This object is generally not directly instantiated, and it does not + directly implement parsing and building, so it is largely only of interest + to subclass implementors. + + The external user API: + + * ``parse()`` + * ``parse_stream()`` + * ``build()`` + * ``build_stream()`` + * ``sizeof()`` + + Subclass authors should not override the external methods. Instead, + another API is available: + + * ``_parse()`` + * ``_build()`` + * ``_sizeof()`` + + There is also a flag API: + + * ``_set_flag()`` + * ``_clear_flag()`` + * ``_inherit_flags()`` + * ``_is_flag()`` + + And stateful copying: + + * ``__getstate__()`` + * ``__setstate__()`` + + Attributes and Inheritance + ========================== + + All constructs have a name and flags. The name is used for naming struct + members and context dictionaries. Note that the name can either be a + string, or None if the name is not needed. A single underscore ("_") is a + reserved name, and so are names starting with a less-than character ("<"). + The name should be descriptive, short, and valid as a Python identifier, + although these rules are not enforced. + + The flags specify additional behavioral information about this construct. + Flags are used by enclosing constructs to determine a proper course of + action. Flags are inherited by default, from inner subconstructs to outer + constructs. The enclosing construct may set new flags or clear existing + ones, as necessary. + + For example, if ``FLAG_COPY_CONTEXT`` is set, repeaters will pass a copy of + the context for each iteration, which is necessary for OnDemand parsing. + """ + + FLAG_COPY_CONTEXT = 0x0001 + FLAG_DYNAMIC = 0x0002 + FLAG_EMBED = 0x0004 + FLAG_NESTING = 0x0008 + + __slots__ = ["name", "conflags"] + def __init__(self, name, flags = 0): + if name is not None: + if not isinstance(name, six.string_types): + raise TypeError("name must be a string or None", name) + if name == "_" or name.startswith("<"): + raise ValueError("reserved name", name) + self.name = name + self.conflags = flags + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.name) + + def _set_flag(self, flag): + """ + Set the given flag or flags. + + :param int flag: flag to set; may be OR'd combination of flags + """ + + self.conflags |= flag + + def _clear_flag(self, flag): + """ + Clear the given flag or flags. + + :param int flag: flag to clear; may be OR'd combination of flags + """ + + self.conflags &= ~flag + + def _inherit_flags(self, *subcons): + """ + Pull flags from subconstructs. + """ + + for sc in subcons: + self._set_flag(sc.conflags) + + def _is_flag(self, flag): + """ + Check whether a given flag is set. + + :param int flag: flag to check + """ + + return bool(self.conflags & flag) + + def __getstate__(self): + """ + Obtain a dictionary representing this construct's state. + """ + + attrs = {} + if hasattr(self, "__dict__"): + attrs.update(self.__dict__) + slots = [] + c = self.__class__ + while c is not None: + if hasattr(c, "__slots__"): + slots.extend(c.__slots__) + c = c.__base__ + for name in slots: + if hasattr(self, name): + attrs[name] = getattr(self, name) + return attrs + + def __setstate__(self, attrs): + """ + Set this construct's state to a given state. + """ + for name, value in attrs.items(): + setattr(self, name, value) + + def __copy__(self): + """returns a copy of this construct""" + self2 = object.__new__(self.__class__) + self2.__setstate__(self, self.__getstate__()) + return self2 + + def parse(self, data): + """ + Parse an in-memory buffer. + + Strings, buffers, memoryviews, and other complete buffers can be + parsed with this method. + """ + + return self.parse_stream(BytesIO(data)) + + def parse_stream(self, stream): + """ + Parse a stream. + + Files, pipes, sockets, and other streaming sources of data are handled + by this method. + """ + + return self._parse(stream, Container()) + + def _parse(self, stream, context): + """ + Override me in your subclass. + """ + + raise NotImplementedError() + + def build(self, obj): + """ + Build an object in memory. + """ + stream = BytesIO() + self.build_stream(obj, stream) + return stream.getvalue() + + def build_stream(self, obj, stream): + """ + Build an object directly into a stream. + """ + self._build(obj, stream, Container()) + + def _build(self, obj, stream, context): + """ + Override me in your subclass. + """ + + raise NotImplementedError() + + def sizeof(self, context=None): + """ + Calculate the size of this object, optionally using a context. + + Some constructs have no fixed size and can only know their size for a + given hunk of data; these constructs will raise an error if they are + not passed a context. + + :param context: contextual data + + :returns: int of the length of this construct + :raises SizeofError: the size could not be determined + """ + + if context is None: + context = Container() + try: + return self._sizeof(context) + except Exception: + raise SizeofError(sys.exc_info()[1]) + + def _sizeof(self, context): + """ + Override me in your subclass. + """ + + raise SizeofError("Raw Constructs have no size!") + +class Subconstruct(Construct): + """ + Abstract subconstruct (wraps an inner construct, inheriting its + name and flags). + + Subconstructs wrap an inner Construct, inheriting its name and flags. + + :param subcon: the construct to wrap + """ + + __slots__ = ["subcon"] + def __init__(self, subcon): + Construct.__init__(self, subcon.name, subcon.conflags) + self.subcon = subcon + def _parse(self, stream, context): + return self.subcon._parse(stream, context) + def _build(self, obj, stream, context): + self.subcon._build(obj, stream, context) + def _sizeof(self, context): + return self.subcon._sizeof(context) + +class Adapter(Subconstruct): + """ + Abstract adapter parent class. + + Adapters should implement ``_decode()`` and ``_encode()``. + + :param subcon: the construct to wrap + """ + + __slots__ = [] + def _parse(self, stream, context): + return self._decode(self.subcon._parse(stream, context), context) + def _build(self, obj, stream, context): + self.subcon._build(self._encode(obj, context), stream, context) + def _decode(self, obj, context): + raise NotImplementedError() + def _encode(self, obj, context): + raise NotImplementedError() + + +#=============================================================================== +# Fields +#=============================================================================== +def _read_stream(stream, length): + if length < 0: + raise ValueError("length must be >= 0", length) + data = stream.read(length) + if len(data) != length: + raise FieldError("expected %d, found %d" % (length, len(data))) + return data + +def _write_stream(stream, length, data): + if length < 0: + raise ValueError("length must be >= 0", length) + if len(data) != length: + raise FieldError("expected %d, found %d" % (length, len(data))) + stream.write(data) + +class StaticField(Construct): + """ + A fixed-size byte field. + + :param name: field name + :param length: number of bytes in the field + """ + + __slots__ = ["length"] + def __init__(self, name, length): + Construct.__init__(self, name) + self.length = length + def _parse(self, stream, context): + return _read_stream(stream, self.length) + def _build(self, obj, stream, context): + _write_stream(stream, self.length, bchr(obj) if isinstance(obj, int) else obj) + def _sizeof(self, context): + return self.length + +class FormatField(StaticField): + """ + A field that uses ``struct`` to pack and unpack data. + + See ``struct`` documentation for instructions on crafting format strings. + + :param name: name of the field + :param endianness: format endianness string; one of "<", ">", or "=" + :param format: a single format character + """ + + __slots__ = ["packer"] + def __init__(self, name, endianity, format): + if endianity not in (">", "<", "="): + raise ValueError("endianity must be be '=', '<', or '>'", + endianity) + if len(format) != 1: + raise ValueError("must specify one and only one format char") + self.packer = Packer(endianity + format) + StaticField.__init__(self, name, self.packer.size) + def __getstate__(self): + attrs = StaticField.__getstate__(self) + attrs["packer"] = attrs["packer"].format + return attrs + def __setstate__(self, attrs): + attrs["packer"] = Packer(attrs["packer"]) + return StaticField.__setstate__(self, attrs) + def _parse(self, stream, context): + try: + return self.packer.unpack(_read_stream(stream, self.length))[0] + except Exception: + raise FieldError(sys.exc_info()[1]) + def _build(self, obj, stream, context): + try: + _write_stream(stream, self.length, self.packer.pack(obj)) + except Exception: + raise FieldError(sys.exc_info()[1]) + +class MetaField(Construct): + r""" + A variable-length field. The length is obtained at runtime from a + function. + + :param name: name of the field + :param lengthfunc: callable that takes a context and returns length as an int + + Example:: + + >>> foo = Struct("foo", + ... Byte("length"), + ... MetaField("data", lambda ctx: ctx["length"]) + ... ) + >>> foo.parse("\x03ABC") + Container(data = 'ABC', length = 3) + >>> foo.parse("\x04ABCD") + Container(data = 'ABCD', length = 4) + """ + + __slots__ = ["lengthfunc"] + def __init__(self, name, lengthfunc): + Construct.__init__(self, name) + self.lengthfunc = lengthfunc + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + return _read_stream(stream, self.lengthfunc(context)) + def _build(self, obj, stream, context): + _write_stream(stream, self.lengthfunc(context), obj) + def _sizeof(self, context): + return self.lengthfunc(context) + + +#=============================================================================== +# arrays and repeaters +#=============================================================================== +class MetaArray(Subconstruct): + """ + An array (repeater) of a meta-count. The array will iterate exactly + ``countfunc()`` times. Will raise ArrayError if less elements are found. + + .. seealso:: + + The :func:`~construct.macros.Array` macro, :func:`Range` and :func:`RepeatUntil`. + + :param countfunc: a function that takes the context as a parameter and returns + the number of elements of the array (count) + :param subcon: the subcon to repeat ``countfunc()`` times + + Example:: + + MetaArray(lambda ctx: 5, UBInt8("foo")) + """ + __slots__ = ["countfunc"] + def __init__(self, countfunc, subcon): + Subconstruct.__init__(self, subcon) + self.countfunc = countfunc + self._clear_flag(self.FLAG_COPY_CONTEXT) + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + obj = ListContainer() + c = 0 + count = self.countfunc(context) + try: + if self.subcon.conflags & self.FLAG_COPY_CONTEXT: + while c < count: + obj.append(self.subcon._parse(stream, context.__copy__())) + c += 1 + else: + while c < count: + obj.append(self.subcon._parse(stream, context)) + c += 1 + except ConstructError: + raise ArrayError("expected %d, found %d" % (count, c), sys.exc_info()[1]) + return obj + def _build(self, obj, stream, context): + count = self.countfunc(context) + if len(obj) != count: + raise ArrayError("expected %d, found %d" % (count, len(obj))) + if self.subcon.conflags & self.FLAG_COPY_CONTEXT: + for subobj in obj: + self.subcon._build(subobj, stream, context.__copy__()) + else: + for subobj in obj: + self.subcon._build(subobj, stream, context) + def _sizeof(self, context): + return self.subcon._sizeof(context) * self.countfunc(context) + +class Range(Subconstruct): + r""" + A range-array. The subcon will iterate between ``mincount`` to ``maxcount`` + times. If less than ``mincount`` elements are found, raises RangeError. + + .. seealso:: + + The :func:`~construct.macros.GreedyRange` and + :func:`~construct.macros.OptionalGreedyRange` macros. + + The general-case repeater. Repeats the given unit for at least ``mincount`` + times, and up to ``maxcount`` times. If an exception occurs (EOF, validation + error), the repeater exits. If less than ``mincount`` units have been + successfully parsed, a RangeError is raised. + + .. note:: This object requires a seekable stream for parsing. + + :param mincount: the minimal count + :param maxcount: the maximal count + :param subcon: the subcon to repeat + + Example:: + + >>> c = Range(3, 7, UBInt8("foo")) + >>> c.parse("\x01\x02") + Traceback (most recent call last): + ... + construct.core.RangeError: expected 3..7, found 2 + >>> c.parse("\x01\x02\x03") + [1, 2, 3] + >>> c.parse("\x01\x02\x03\x04\x05\x06") + [1, 2, 3, 4, 5, 6] + >>> c.parse("\x01\x02\x03\x04\x05\x06\x07") + [1, 2, 3, 4, 5, 6, 7] + >>> c.parse("\x01\x02\x03\x04\x05\x06\x07\x08\x09") + [1, 2, 3, 4, 5, 6, 7] + >>> c.build([1,2]) + Traceback (most recent call last): + ... + construct.core.RangeError: expected 3..7, found 2 + >>> c.build([1,2,3,4]) + '\x01\x02\x03\x04' + >>> c.build([1,2,3,4,5,6,7,8]) + Traceback (most recent call last): + ... + construct.core.RangeError: expected 3..7, found 8 + """ + + __slots__ = ["mincount", "maxcout"] + def __init__(self, mincount, maxcout, subcon): + Subconstruct.__init__(self, subcon) + self.mincount = mincount + self.maxcout = maxcout + self._clear_flag(self.FLAG_COPY_CONTEXT) + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + obj = ListContainer() + c = 0 + try: + if self.subcon.conflags & self.FLAG_COPY_CONTEXT: + while c < self.maxcout: + pos = stream.tell() + obj.append(self.subcon._parse(stream, context.__copy__())) + c += 1 + else: + while c < self.maxcout: + pos = stream.tell() + obj.append(self.subcon._parse(stream, context)) + c += 1 + except ConstructError: + if c < self.mincount: + raise RangeError("expected %d to %d, found %d" % + (self.mincount, self.maxcout, c), sys.exc_info()[1]) + stream.seek(pos) + return obj + def _build(self, obj, stream, context): + if len(obj) < self.mincount or len(obj) > self.maxcout: + raise RangeError("expected %d to %d, found %d" % + (self.mincount, self.maxcout, len(obj))) + cnt = 0 + try: + if self.subcon.conflags & self.FLAG_COPY_CONTEXT: + for subobj in obj: + if isinstance(obj, bytes): + subobj = bchr(subobj) + self.subcon._build(subobj, stream, context.__copy__()) + cnt += 1 + else: + for subobj in obj: + if isinstance(obj, bytes): + subobj = bchr(subobj) + self.subcon._build(subobj, stream, context) + cnt += 1 + except ConstructError: + if cnt < self.mincount: + raise RangeError("expected %d to %d, found %d" % + (self.mincount, self.maxcout, len(obj)), sys.exc_info()[1]) + def _sizeof(self, context): + raise SizeofError("can't calculate size") + +class RepeatUntil(Subconstruct): + r""" + An array that repeats until the predicate indicates it to stop. Note that + the last element (which caused the repeat to exit) is included in the + return value. + + :param predicate: a predicate function that takes (obj, context) and returns + True if the stop-condition is met, or False to continue. + :param subcon: the subcon to repeat. + + Example:: + + # will read chars until '\x00' (inclusive) + RepeatUntil(lambda obj, ctx: obj == b"\x00", + Field("chars", 1) + ) + """ + __slots__ = ["predicate"] + def __init__(self, predicate, subcon): + Subconstruct.__init__(self, subcon) + self.predicate = predicate + self._clear_flag(self.FLAG_COPY_CONTEXT) + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + obj = [] + try: + if self.subcon.conflags & self.FLAG_COPY_CONTEXT: + while True: + subobj = self.subcon._parse(stream, context.__copy__()) + obj.append(subobj) + if self.predicate(subobj, context): + break + else: + while True: + subobj = self.subcon._parse(stream, context) + obj.append(subobj) + if self.predicate(subobj, context): + break + except ConstructError: + raise ArrayError("missing terminator", sys.exc_info()[1]) + return obj + def _build(self, obj, stream, context): + terminated = False + if self.subcon.conflags & self.FLAG_COPY_CONTEXT: + for subobj in obj: + self.subcon._build(subobj, stream, context.__copy__()) + if self.predicate(subobj, context): + terminated = True + break + else: + for subobj in obj: + #subobj = bchr(subobj) -- WTF is that for?! + self.subcon._build(subobj, stream, context.__copy__()) + if self.predicate(subobj, context): + terminated = True + break + if not terminated: + raise ArrayError("missing terminator") + def _sizeof(self, context): + raise SizeofError("can't calculate size") + + +#=============================================================================== +# structures and sequences +#=============================================================================== +class Struct(Construct): + """ + A sequence of named constructs, similar to structs in C. The elements are + parsed and built in the order they are defined. + + .. seealso:: The :func:`~construct.macros.Embedded` macro. + + :param name: the name of the structure + :param subcons: a sequence of subconstructs that make up this structure. + :param nested: a keyword-only argument that indicates whether this struct + creates a nested context. The default is True. This parameter is + considered "advanced usage", and may be removed in the future. + + Example:: + + Struct("foo", + UBInt8("first_element"), + UBInt16("second_element"), + Padding(2), + UBInt8("third_element"), + ) + """ + __slots__ = ["subcons", "nested", "allow_overwrite"] + def __init__(self, name, *subcons, **kw): + self.nested = kw.pop("nested", True) + self.allow_overwrite = kw.pop("allow_overwrite", False) + if kw: + raise TypeError("the only keyword argument accepted is 'nested'", kw) + Construct.__init__(self, name) + self.subcons = subcons + self._inherit_flags(*subcons) + self._clear_flag(self.FLAG_EMBED) + def _parse(self, stream, context): + if "<obj>" in context: + obj = context["<obj>"] + del context["<obj>"] + else: + obj = Container() + if self.nested: + context = Container(_ = context) + for sc in self.subcons: + if sc.conflags & self.FLAG_EMBED: + context["<obj>"] = obj + sc._parse(stream, context) + else: + subobj = sc._parse(stream, context) + if sc.name is not None: + if sc.name in obj and not self.allow_overwrite: + raise OverwriteError("%r would be overwritten but allow_overwrite is False" % (sc.name,)) + obj[sc.name] = subobj + context[sc.name] = subobj + return obj + def _build(self, obj, stream, context): + if "<unnested>" in context: + del context["<unnested>"] + elif self.nested: + context = Container(_ = context) + for sc in self.subcons: + if sc.conflags & self.FLAG_EMBED: + context["<unnested>"] = True + subobj = obj + elif sc.name is None: + subobj = None + else: + subobj = getattr(obj, sc.name) + context[sc.name] = subobj + sc._build(subobj, stream, context) + def _sizeof(self, context): + #if self.nested: + # context = Container(_ = context) + return sum(sc._sizeof(context) for sc in self.subcons) + +class Sequence(Struct): + """ + A sequence of unnamed constructs. The elements are parsed and built in the + order they are defined. + + .. seealso:: The :func:`~construct.macros.Embedded` macro. + + :param name: the name of the structure + :param subcons: a sequence of subconstructs that make up this structure. + :param nested: a keyword-only argument that indicates whether this struct + creates a nested context. The default is True. This parameter is + considered "advanced usage", and may be removed in the future. + + Example:: + + Sequence("foo", + UBInt8("first_element"), + UBInt16("second_element"), + Padding(2), + UBInt8("third_element"), + ) + """ + __slots__ = [] + def _parse(self, stream, context): + if "<obj>" in context: + obj = context["<obj>"] + del context["<obj>"] + else: + obj = ListContainer() + if self.nested: + context = Container(_ = context) + for sc in self.subcons: + if sc.conflags & self.FLAG_EMBED: + context["<obj>"] = obj + sc._parse(stream, context) + else: + subobj = sc._parse(stream, context) + if sc.name is not None: + obj.append(subobj) + context[sc.name] = subobj + return obj + def _build(self, obj, stream, context): + if "<unnested>" in context: + del context["<unnested>"] + elif self.nested: + context = Container(_ = context) + objiter = iter(obj) + for sc in self.subcons: + if sc.conflags & self.FLAG_EMBED: + context["<unnested>"] = True + subobj = objiter + elif sc.name is None: + subobj = None + else: + subobj = advance_iterator(objiter) + context[sc.name] = subobj + sc._build(subobj, stream, context) + +class Union(Construct): + """ + a set of overlapping fields (like unions in C). when parsing, + all fields read the same data; when building, only the first subcon + (called "master") is used. + + :param name: the name of the union + :param master: the master subcon, i.e., the subcon used for building and calculating the total size + :param subcons: additional subcons + + Example:: + + Union("what_are_four_bytes", + UBInt32("one_dword"), + Struct("two_words", UBInt16("first"), UBInt16("second")), + Struct("four_bytes", + UBInt8("a"), + UBInt8("b"), + UBInt8("c"), + UBInt8("d") + ), + ) + """ + __slots__ = ["parser", "builder"] + def __init__(self, name, master, *subcons, **kw): + Construct.__init__(self, name) + args = [Peek(sc) for sc in subcons] + args.append(MetaField(None, lambda ctx: master._sizeof(ctx))) + self.parser = Struct(name, Peek(master, perform_build = True), *args) + self.builder = Struct(name, master) + def _parse(self, stream, context): + return self.parser._parse(stream, context) + def _build(self, obj, stream, context): + return self.builder._build(obj, stream, context) + def _sizeof(self, context): + return self.builder._sizeof(context) + +#=============================================================================== +# conditional +#=============================================================================== +class Switch(Construct): + """ + A conditional branch. Switch will choose the case to follow based on + the return value of keyfunc. If no case is matched, and no default value + is given, SwitchError will be raised. + + .. seealso:: :func:`Pass`. + + :param name: the name of the construct + :param keyfunc: a function that takes the context and returns a key, which + will be used to choose the relevant case. + :param cases: a dictionary mapping keys to constructs. the keys can be any + values that may be returned by keyfunc. + :param default: a default value to use when the key is not found in the cases. + if not supplied, an exception will be raised when the key is not found. + You can use the builtin construct Pass for 'do-nothing'. + :param include_key: whether or not to include the key in the return value + of parsing. defualt is False. + + Example:: + + Struct("foo", + UBInt8("type"), + Switch("value", lambda ctx: ctx.type, { + 1 : UBInt8("spam"), + 2 : UBInt16("spam"), + 3 : UBInt32("spam"), + 4 : UBInt64("spam"), + } + ), + ) + """ + + class NoDefault(Construct): + def _parse(self, stream, context): + raise SwitchError("no default case defined") + def _build(self, obj, stream, context): + raise SwitchError("no default case defined") + def _sizeof(self, context): + raise SwitchError("no default case defined") + NoDefault = NoDefault("No default value specified") + + __slots__ = ["subcons", "keyfunc", "cases", "default", "include_key"] + + def __init__(self, name, keyfunc, cases, default = NoDefault, + include_key = False): + Construct.__init__(self, name) + self._inherit_flags(*cases.values()) + self.keyfunc = keyfunc + self.cases = cases + self.default = default + self.include_key = include_key + self._inherit_flags(*cases.values()) + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + key = self.keyfunc(context) + obj = self.cases.get(key, self.default)._parse(stream, context) + if self.include_key: + return key, obj + else: + return obj + def _build(self, obj, stream, context): + if self.include_key: + key, obj = obj + else: + key = self.keyfunc(context) + case = self.cases.get(key, self.default) + case._build(obj, stream, context) + def _sizeof(self, context): + case = self.cases.get(self.keyfunc(context), self.default) + return case._sizeof(context) + +class Select(Construct): + """ + Selects the first matching subconstruct. It will literally try each of + the subconstructs, until one matches. + + .. note:: Requires a seekable stream. + + :param name: the name of the construct + :param subcons: the subcons to try (order-sensitive) + :param include_name: a keyword only argument, indicating whether to include + the name of the selected subcon in the return value of parsing. default + is false. + + Example:: + + Select("foo", + UBInt64("large"), + UBInt32("medium"), + UBInt16("small"), + UBInt8("tiny"), + ) + """ + __slots__ = ["subcons", "include_name"] + def __init__(self, name, *subcons, **kw): + include_name = kw.pop("include_name", False) + if kw: + raise TypeError("the only keyword argument accepted " + "is 'include_name'", kw) + Construct.__init__(self, name) + self.subcons = subcons + self.include_name = include_name + self._inherit_flags(*subcons) + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + for sc in self.subcons: + pos = stream.tell() + context2 = context.__copy__() + try: + obj = sc._parse(stream, context2) + except ConstructError: + stream.seek(pos) + else: + context.__update__(context2) + if self.include_name: + return sc.name, obj + else: + return obj + raise SelectError("no subconstruct matched") + def _build(self, obj, stream, context): + if self.include_name: + name, obj = obj + for sc in self.subcons: + if sc.name == name: + sc._build(obj, stream, context) + return + else: + for sc in self.subcons: + stream2 = BytesIO() + context2 = context.__copy__() + try: + sc._build(obj, stream2, context2) + except Exception: + pass + else: + context.__update__(context2) + stream.write(stream2.getvalue()) + return + raise SelectError("no subconstruct matched", obj) + def _sizeof(self, context): + raise SizeofError("can't calculate size") + + +#=============================================================================== +# stream manipulation +#=============================================================================== +class Pointer(Subconstruct): + """ + Changes the stream position to a given offset, where the construction + should take place, and restores the stream position when finished. + + .. seealso:: + :func:`Anchor`, :func:`OnDemand` and the + :func:`~construct.macros.OnDemandPointer` macro. + + .. note:: Requires a seekable stream. + + :param offsetfunc: a function that takes the context and returns an absolute + stream position, where the construction would take place + :param subcon: the subcon to use at ``offsetfunc()`` + + Example:: + + Struct("foo", + UBInt32("spam_pointer"), + Pointer(lambda ctx: ctx.spam_pointer, + Array(5, UBInt8("spam")) + ) + ) + """ + __slots__ = ["offsetfunc"] + def __init__(self, offsetfunc, subcon): + Subconstruct.__init__(self, subcon) + self.offsetfunc = offsetfunc + def _parse(self, stream, context): + newpos = self.offsetfunc(context) + origpos = stream.tell() + stream.seek(newpos, 2 if newpos < 0 else 0) + obj = self.subcon._parse(stream, context) + stream.seek(origpos) + return obj + def _build(self, obj, stream, context): + newpos = self.offsetfunc(context) + origpos = stream.tell() + stream.seek(newpos, 2 if newpos < 0 else 0) + self.subcon._build(obj, stream, context) + stream.seek(origpos) + def _sizeof(self, context): + return 0 + +class Peek(Subconstruct): + """ + Peeks at the stream: parses without changing the stream position. + See also Union. If the end of the stream is reached when peeking, + returns None. + + .. note:: Requires a seekable stream. + + :param subcon: the subcon to peek at + :param perform_build: whether or not to perform building. by default this + parameter is set to False, meaning building is a no-op. + + Example:: + + Peek(UBInt8("foo")) + """ + __slots__ = ["perform_build"] + def __init__(self, subcon, perform_build = False): + Subconstruct.__init__(self, subcon) + self.perform_build = perform_build + def _parse(self, stream, context): + pos = stream.tell() + try: + return self.subcon._parse(stream, context) + except FieldError: + pass + finally: + stream.seek(pos) + def _build(self, obj, stream, context): + if self.perform_build: + self.subcon._build(obj, stream, context) + def _sizeof(self, context): + return 0 + +class OnDemand(Subconstruct): + """ + Allows for on-demand (lazy) parsing. When parsing, it will return a + LazyContainer that represents a pointer to the data, but does not actually + parses it from stream until it's "demanded". + By accessing the 'value' property of LazyContainers, you will demand the + data from the stream. The data will be parsed and cached for later use. + You can use the 'has_value' property to know whether the data has already + been demanded. + + .. seealso:: The :func:`~construct.macros.OnDemandPointer` macro. + + .. note:: Requires a seekable stream. + + :param subcon: the subcon to read/write on demand + :param advance_stream: whether or not to advance the stream position. by + default this is True, but if subcon is a pointer, this should be False. + :param force_build: whether or not to force build. If set to False, and the + LazyContainer has not been demaned, building is a no-op. + + Example:: + + OnDemand(Array(10000, UBInt8("foo")) + """ + __slots__ = ["advance_stream", "force_build"] + def __init__(self, subcon, advance_stream = True, force_build = True): + Subconstruct.__init__(self, subcon) + self.advance_stream = advance_stream + self.force_build = force_build + def _parse(self, stream, context): + obj = LazyContainer(self.subcon, stream, stream.tell(), context) + if self.advance_stream: + stream.seek(self.subcon._sizeof(context), 1) + return obj + def _build(self, obj, stream, context): + if not isinstance(obj, LazyContainer): + self.subcon._build(obj, stream, context) + elif self.force_build or obj.has_value: + self.subcon._build(obj.value, stream, context) + elif self.advance_stream: + stream.seek(self.subcon._sizeof(context), 1) + +class Buffered(Subconstruct): + """ + Creates an in-memory buffered stream, which can undergo encoding and + decoding prior to being passed on to the subconstruct. + + .. seealso:: The :func:`~construct.macros.Bitwise` macro. + + .. warning:: Do not use pointers inside ``Buffered``. + + :param subcon: the subcon which will operate on the buffer + :param encoder: a function that takes a string and returns an encoded + string (used after building) + :param decoder: a function that takes a string and returns a decoded + string (used before parsing) + :param resizer: a function that takes the size of the subcon and "adjusts" + or "resizes" it according to the encoding/decoding process. + + Example:: + + Buffered(BitField("foo", 16), + encoder = decode_bin, + decoder = encode_bin, + resizer = lambda size: size / 8, + ) + """ + __slots__ = ["encoder", "decoder", "resizer"] + def __init__(self, subcon, decoder, encoder, resizer): + Subconstruct.__init__(self, subcon) + self.encoder = encoder + self.decoder = decoder + self.resizer = resizer + def _parse(self, stream, context): + data = _read_stream(stream, self._sizeof(context)) + stream2 = BytesIO(self.decoder(data)) + return self.subcon._parse(stream2, context) + def _build(self, obj, stream, context): + size = self._sizeof(context) + stream2 = BytesIO() + self.subcon._build(obj, stream2, context) + data = self.encoder(stream2.getvalue()) + assert len(data) == size + _write_stream(stream, self._sizeof(context), data) + def _sizeof(self, context): + return self.resizer(self.subcon._sizeof(context)) + +class Restream(Subconstruct): + """ + Wraps the stream with a read-wrapper (for parsing) or a + write-wrapper (for building). The stream wrapper can buffer the data + internally, reading it from- or writing it to the underlying stream + as needed. For example, BitStreamReader reads whole bytes from the + underlying stream, but returns them as individual bits. + + .. seealso:: The :func:`~construct.macros.Bitwise` macro. + + When the parsing or building is done, the stream's close method + will be invoked. It can perform any finalization needed for the stream + wrapper, but it must not close the underlying stream. + + .. warning:: Do not use pointers inside ``Restream``. + + :param subcon: the subcon + :param stream_reader: the read-wrapper + :param stream_writer: the write wrapper + :param resizer: a function that takes the size of the subcon and "adjusts" + or "resizes" it according to the encoding/decoding process. + + Example:: + + Restream(BitField("foo", 16), + stream_reader = BitStreamReader, + stream_writer = BitStreamWriter, + resizer = lambda size: size / 8, + ) + """ + __slots__ = ["stream_reader", "stream_writer", "resizer"] + def __init__(self, subcon, stream_reader, stream_writer, resizer): + Subconstruct.__init__(self, subcon) + self.stream_reader = stream_reader + self.stream_writer = stream_writer + self.resizer = resizer + def _parse(self, stream, context): + stream2 = self.stream_reader(stream) + obj = self.subcon._parse(stream2, context) + stream2.close() + return obj + def _build(self, obj, stream, context): + stream2 = self.stream_writer(stream) + self.subcon._build(obj, stream2, context) + stream2.close() + def _sizeof(self, context): + return self.resizer(self.subcon._sizeof(context)) + + +#=============================================================================== +# miscellaneous +#=============================================================================== +class Reconfig(Subconstruct): + """ + Reconfigures a subconstruct. Reconfig can be used to change the name and + set and clear flags of the inner subcon. + + :param name: the new name + :param subcon: the subcon to reconfigure + :param setflags: the flags to set (default is 0) + :param clearflags: the flags to clear (default is 0) + + Example:: + + Reconfig("foo", UBInt8("bar")) + """ + __slots__ = [] + def __init__(self, name, subcon, setflags = 0, clearflags = 0): + Construct.__init__(self, name, subcon.conflags) + self.subcon = subcon + self._set_flag(setflags) + self._clear_flag(clearflags) + +class Anchor(Construct): + """ + Gets the *anchor* (stream position) at a point in a Construct. + + Anchors are useful for adjusting relative offsets to absolute positions, + or to measure sizes of Constructs. + + To get an absolute pointer, use an Anchor plus a relative offset. To get a + size, place two Anchors and measure their difference. + + :param name: the name of the anchor + + .. note:: + + Anchor Requires a seekable stream, or at least a tellable stream; it is + implemented using the ``tell()`` method of file-like objects. + + .. seealso:: :func:`Pointer` + """ + + __slots__ = [] + def _parse(self, stream, context): + return stream.tell() + def _build(self, obj, stream, context): + context[self.name] = stream.tell() + def _sizeof(self, context): + return 0 + +class Value(Construct): + """ + A computed value. + + :param name: the name of the value + :param func: a function that takes the context and return the computed value + + Example:: + + Struct("foo", + UBInt8("width"), + UBInt8("height"), + Value("total_pixels", lambda ctx: ctx.width * ctx.height), + ) + """ + __slots__ = ["func"] + def __init__(self, name, func): + Construct.__init__(self, name) + self.func = func + self._set_flag(self.FLAG_DYNAMIC) + def _parse(self, stream, context): + return self.func(context) + def _build(self, obj, stream, context): + context[self.name] = self.func(context) + def _sizeof(self, context): + return 0 + +#class Dynamic(Construct): +# """ +# Dynamically creates a construct and uses it for parsing and building. +# This allows you to create change the construction tree on the fly. +# Deprecated. +# +# Parameters: +# * name - the name of the construct +# * factoryfunc - a function that takes the context and returns a new +# construct object which will be used for parsing and building. +# +# Example: +# def factory(ctx): +# if ctx.bar == 8: +# return UBInt8("spam") +# if ctx.bar == 9: +# return String("spam", 9) +# +# Struct("foo", +# UBInt8("bar"), +# Dynamic("spam", factory), +# ) +# """ +# __slots__ = ["factoryfunc"] +# def __init__(self, name, factoryfunc): +# Construct.__init__(self, name, self.FLAG_COPY_CONTEXT) +# self.factoryfunc = factoryfunc +# self._set_flag(self.FLAG_DYNAMIC) +# def _parse(self, stream, context): +# return self.factoryfunc(context)._parse(stream, context) +# def _build(self, obj, stream, context): +# return self.factoryfunc(context)._build(obj, stream, context) +# def _sizeof(self, context): +# return self.factoryfunc(context)._sizeof(context) + +class LazyBound(Construct): + """ + Lazily bound construct, useful for constructs that need to make cyclic + references (linked-lists, expression trees, etc.). + + :param name: the name of the construct + :param bindfunc: the function (called without arguments) returning the bound construct + + Example:: + + foo = Struct("foo", + UBInt8("bar"), + LazyBound("next", lambda: foo), + ) + """ + __slots__ = ["bindfunc", "bound"] + def __init__(self, name, bindfunc): + Construct.__init__(self, name) + self.bound = None + self.bindfunc = bindfunc + def _parse(self, stream, context): + if self.bound is None: + self.bound = self.bindfunc() + return self.bound._parse(stream, context) + def _build(self, obj, stream, context): + if self.bound is None: + self.bound = self.bindfunc() + self.bound._build(obj, stream, context) + def _sizeof(self, context): + if self.bound is None: + self.bound = self.bindfunc() + return self.bound._sizeof(context) + +class Pass(Construct): + """ + A do-nothing construct, useful as the default case for Switch, or + to indicate Enums. + + .. seealso:: :func:`Switch` and the :func:`~construct.macros.Enum` macro. + + .. note:: This construct is a singleton. Do not try to instatiate it, as it will not work. + + Example:: + + Pass + """ + __slots__ = [] + def _parse(self, stream, context): + pass + def _build(self, obj, stream, context): + assert obj is None + def _sizeof(self, context): + return 0 + +Pass = Pass(None) +""" +A do-nothing construct, useful as the default case for Switch, or +to indicate Enums. + +.. seealso:: :func:`Switch` and the :func:`~construct.macros.Enum` macro. + +.. note:: This construct is a singleton. Do not try to instatiate it, as it will not work. + +Example:: + + Pass +""" + +class Terminator(Construct): + """ + Asserts the end of the stream has been reached at the point it's placed. + You can use this to ensure no more unparsed data follows. + + .. note:: + * This construct is only meaningful for parsing. For building, it's a no-op. + * This construct is a singleton. Do not try to instatiate it, as it will not work. + + Example:: + + Terminator + """ + __slots__ = [] + def _parse(self, stream, context): + if stream.read(1): + raise TerminatorError("expected end of stream") + def _build(self, obj, stream, context): + assert obj is None + def _sizeof(self, context): + return 0 + +Terminator = Terminator(None) +""" +Asserts the end of the stream has been reached at the point it's placed. +You can use this to ensure no more unparsed data follows. + +.. note:: + * This construct is only meaningful for parsing. For building, it's a no-op. + * This construct is a singleton. Do not try to instatiate it, as it will not work. + +Example:: + + Terminator +""" + +#======================================================================================================================= +# Extra +#======================================================================================================================= +class ULInt24(StaticField): + """ + A custom made construct for handling 3-byte types as used in ancient file formats. + A better implementation would be writing a more flexable version of FormatField, + rather then specifically implementing it for this case + """ + __slots__ = ["packer"] + def __init__(self, name): + self.packer = Packer("<BH") + StaticField.__init__(self, name, self.packer.size) + def __getstate__(self): + attrs = StaticField.__getstate__(self) + attrs["packer"] = attrs["packer"].format + return attrs + def __setstate__(self, attrs): + attrs["packer"] = Packer(attrs["packer"]) + return StaticField.__setstate__(self, attrs) + def _parse(self, stream, context): + try: + vals = self.packer.unpack(_read_stream(stream, self.length)) + return vals[0] + (vals[1] << 8) + except Exception: + ex = sys.exc_info()[1] + raise FieldError(ex) + def _build(self, obj, stream, context): + try: + vals = (obj%256, obj >> 8) + _write_stream(stream, self.length, self.packer.pack(vals)) + except Exception: + ex = sys.exc_info()[1] + raise FieldError(ex) + + + + diff --git a/external/construct/core.pyc b/external/construct/core.pyc Binary files differnew file mode 100644 index 0000000..7bc1225 --- /dev/null +++ b/external/construct/core.pyc diff --git a/external/construct/debug.py b/external/construct/debug.py new file mode 100644 index 0000000..3910cae --- /dev/null +++ b/external/construct/debug.py @@ -0,0 +1,131 @@ +""" +Debugging utilities for constructs +""" +import sys +import traceback +import pdb +import inspect +from construct.core import Construct, Subconstruct +from construct.lib import HexString, Container, ListContainer + + +class Probe(Construct): + """ + A probe: dumps the context, stack frames, and stream content to the screen + to aid the debugging process. + + .. seealso:: :class:`Debugger`. + + :param name: the display name + :param show_stream: whether or not to show stream contents. default is True. the stream must be seekable. + :param show_context: whether or not to show the context. default is True. + :param show_stack: whether or not to show the upper stack frames. default is True. + :param stream_lookahead: the number of bytes to dump when show_stack is set. default is 100. + + Example:: + + Struct("foo", + UBInt8("a"), + Probe("between a and b"), + UBInt8("b"), + ) + """ + __slots__ = [ + "printname", "show_stream", "show_context", "show_stack", + "stream_lookahead" + ] + counter = 0 + + def __init__(self, name = None, show_stream = True, + show_context = True, show_stack = True, + stream_lookahead = 100): + Construct.__init__(self, None) + if name is None: + Probe.counter += 1 + name = "<unnamed %d>" % (Probe.counter,) + self.printname = name + self.show_stream = show_stream + self.show_context = show_context + self.show_stack = show_stack + self.stream_lookahead = stream_lookahead + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.printname) + def _parse(self, stream, context): + self.printout(stream, context) + def _build(self, obj, stream, context): + self.printout(stream, context) + def _sizeof(self, context): + return 0 + + def printout(self, stream, context): + obj = Container() + if self.show_stream: + obj.stream_position = stream.tell() + follows = stream.read(self.stream_lookahead) + if not follows: + obj.following_stream_data = "EOF reached" + else: + stream.seek(-len(follows), 1) + obj.following_stream_data = HexString(follows) + print("") + + if self.show_context: + obj.context = context + + if self.show_stack: + obj.stack = ListContainer() + frames = [s[0] for s in inspect.stack()][1:-1] + frames.reverse() + for f in frames: + a = Container() + a.__update__(f.f_locals) + obj.stack.append(a) + + print("=" * 80) + print("Probe %s" % (self.printname,)) + print(obj) + print("=" * 80) + +class Debugger(Subconstruct): + """ + A pdb-based debugger. When an exception occurs in the subcon, a debugger + will appear and allow you to debug the error (and even fix on-the-fly). + + :param subcon: the subcon to debug + + Example:: + + Debugger( + Enum(UBInt8("foo"), + a = 1, + b = 2, + c = 3 + ) + ) + """ + __slots__ = ["retval"] + def _parse(self, stream, context): + try: + return self.subcon._parse(stream, context) + except Exception: + self.retval = NotImplemented + self.handle_exc("(you can set the value of 'self.retval', " + "which will be returned)") + if self.retval is NotImplemented: + raise + else: + return self.retval + def _build(self, obj, stream, context): + try: + self.subcon._build(obj, stream, context) + except Exception: + self.handle_exc() + def handle_exc(self, msg = None): + print("=" * 80) + print("Debugging exception of %s:" % (self.subcon,)) + print("".join(traceback.format_exception(*sys.exc_info())[1:])) + if msg: + print(msg) + pdb.post_mortem(sys.exc_info()[2]) + print("=" * 80) + diff --git a/external/construct/debug.pyc b/external/construct/debug.pyc Binary files differnew file mode 100644 index 0000000..abecc69 --- /dev/null +++ b/external/construct/debug.pyc diff --git a/external/construct/formats/__init__.py b/external/construct/formats/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/construct/formats/__init__.py diff --git a/external/construct/formats/data/__init__.py b/external/construct/formats/data/__init__.py new file mode 100644 index 0000000..50ce2de --- /dev/null +++ b/external/construct/formats/data/__init__.py @@ -0,0 +1,3 @@ +""" +all sorts of raw data serialization (tcpdump capture files, etc.) +""" diff --git a/external/construct/formats/data/cap.py b/external/construct/formats/data/cap.py new file mode 100644 index 0000000..f95c5c1 --- /dev/null +++ b/external/construct/formats/data/cap.py @@ -0,0 +1,55 @@ +""" +tcpdump capture file +""" +from construct import * +import time +from datetime import datetime + + +class MicrosecAdapter(Adapter): + def _decode(self, obj, context): + return datetime.fromtimestamp(obj[0] + (obj[1] / 1000000.0)) + def _encode(self, obj, context): + offset = time.mktime(*obj.timetuple()) + sec = int(offset) + usec = (offset - sec) * 1000000 + return (sec, usec) + +packet = Struct("packet", + MicrosecAdapter( + Sequence("time", + ULInt32("time"), + ULInt32("usec"), + ) + ), + ULInt32("length"), + Padding(4), + HexDumpAdapter(Field("data", lambda ctx: ctx.length)), +) + +cap_file = Struct("cap_file", + Padding(24), + Rename("packets", OptionalGreedyRange(packet)), +) + + +if __name__ == "__main__": + obj = cap_file.parse_stream(open("../../tests/cap2.cap", "rb")) + print(len(obj.packets)) + + + + + + + + + + + + + + + + + diff --git a/external/construct/formats/data/snoop.py b/external/construct/formats/data/snoop.py new file mode 100644 index 0000000..a5fa799 --- /dev/null +++ b/external/construct/formats/data/snoop.py @@ -0,0 +1,50 @@ +""" +what : snoop v2 capture file. + how : http://tools.ietf.org/html/rfc1761 + who : jesse @ housejunkie . ca +""" + +import time +from construct import (Adapter, Enum, Field, HexDumpAdapter, Magic, OptionalGreedyRange, + Padding, Struct, UBInt32) + +class EpochTimeStampAdapter(Adapter): + """ Convert epoch timestamp <-> localtime """ + + def _decode(self, obj, context): + return time.ctime(obj) + def _encode(self, obj, context): + return int(time.mktime(time.strptime(obj))) + +packet_record = Struct("packet_record", + UBInt32("original_length"), + UBInt32("included_length"), + UBInt32("record_length"), + UBInt32("cumulative_drops"), + EpochTimeStampAdapter(UBInt32("timestamp_seconds")), + UBInt32("timestamp_microseconds"), + HexDumpAdapter(Field("data", lambda ctx: ctx.included_length)), + # 24 being the static length of the packet_record header + Padding(lambda ctx: ctx.record_length - ctx.included_length - 24), + ) + +datalink_type = Enum(UBInt32("datalink"), + IEEE802dot3 = 0, + IEEE802dot4 = 1, + IEEE802dot5 = 2, + IEEE802dot6 = 3, + ETHERNET = 4, + HDLC = 5, + CHARSYNC = 6, + IBMCHANNEL = 7, + FDDI = 8, + OTHER = 9, + UNASSIGNED = 10, + ) + +snoop_file = Struct("snoop", + Magic("snoop\x00\x00\x00"), + UBInt32("version"), # snoop v1 is deprecated + datalink_type, + OptionalGreedyRange(packet_record), + ) diff --git a/external/construct/formats/executable/__init__.py b/external/construct/formats/executable/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/construct/formats/executable/__init__.py diff --git a/external/construct/formats/executable/elf32.py b/external/construct/formats/executable/elf32.py new file mode 100644 index 0000000..246ae32 --- /dev/null +++ b/external/construct/formats/executable/elf32.py @@ -0,0 +1,154 @@ +""" +Executable and Linkable Format (ELF), 32 bit, big or little endian +Used on *nix systems as a replacement of the older a.out format + +Big-endian support kindly submitted by Craig McQueen (mcqueen-c#edsrd1!yzk!co!jp) +""" +from construct import * +import six + + +def elf32_body(ElfInt16, ElfInt32): + elf32_program_header = Struct("program_header", + Enum(ElfInt32("type"), + NULL = 0, + LOAD = 1, + DYNAMIC = 2, + INTERP = 3, + NOTE = 4, + SHLIB = 5, + PHDR = 6, + _default_ = Pass, + ), + ElfInt32("offset"), + ElfInt32("vaddr"), + ElfInt32("paddr"), + ElfInt32("file_size"), + ElfInt32("mem_size"), + ElfInt32("flags"), + ElfInt32("align"), + ) + + elf32_section_header = Struct("section_header", + ElfInt32("name_offset"), + Pointer(lambda ctx: ctx._.strtab_data_offset + ctx.name_offset, + CString("name") + ), + Enum(ElfInt32("type"), + NULL = 0, + PROGBITS = 1, + SYMTAB = 2, + STRTAB = 3, + RELA = 4, + HASH = 5, + DYNAMIC = 6, + NOTE = 7, + NOBITS = 8, + REL = 9, + SHLIB = 10, + DYNSYM = 11, + _default_ = Pass, + ), + ElfInt32("flags"), + ElfInt32("addr"), + ElfInt32("offset"), + ElfInt32("size"), + ElfInt32("link"), + ElfInt32("info"), + ElfInt32("align"), + ElfInt32("entry_size"), + OnDemandPointer(lambda ctx: ctx.offset, + HexDumpAdapter(Field("data", lambda ctx: ctx.size)) + ), + ) + + return Struct("body", + Enum(ElfInt16("type"), + NONE = 0, + RELOCATABLE = 1, + EXECUTABLE = 2, + SHARED = 3, + CORE = 4, + ), + Enum(ElfInt16("machine"), + NONE = 0, + M32 = 1, + SPARC = 2, + I386 = 3, + Motorolla68K = 4, + Motorolla88K = 5, + Intel860 = 7, + MIPS = 8, + _default_ = Pass + ), + ElfInt32("version"), + ElfInt32("entry"), + ElfInt32("ph_offset"), + ElfInt32("sh_offset"), + ElfInt32("flags"), + ElfInt16("header_size"), + ElfInt16("ph_entry_size"), + ElfInt16("ph_count"), + ElfInt16("sh_entry_size"), + ElfInt16("sh_count"), + ElfInt16("strtab_section_index"), + + # calculate the string table data offset (pointer arithmetics) + # ugh... anyway, we need it in order to read the section names, later on + Pointer(lambda ctx: + ctx.sh_offset + ctx.strtab_section_index * ctx.sh_entry_size + 16, + ElfInt32("strtab_data_offset"), + ), + + # program header table + Rename("program_table", + Pointer(lambda ctx: ctx.ph_offset, + Array(lambda ctx: ctx.ph_count, + elf32_program_header + ) + ) + ), + + # section table + Rename("sections", + Pointer(lambda ctx: ctx.sh_offset, + Array(lambda ctx: ctx.sh_count, + elf32_section_header + ) + ) + ), + ) + +elf32_body_little_endian = elf32_body(ULInt16, ULInt32) +elf32_body_big_endian = elf32_body(UBInt16, UBInt32) + +elf32_file = Struct("elf32_file", + Struct("identifier", + Magic(six.b("\x7fELF")), + Enum(Byte("file_class"), + NONE = 0, + CLASS32 = 1, + CLASS64 = 2, + ), + Enum(Byte("encoding"), + NONE = 0, + LSB = 1, + MSB = 2, + ), + Byte("version"), + Padding(9), + ), + Embedded(IfThenElse("body", lambda ctx: ctx.identifier.encoding == "LSB", + elf32_body_little_endian, + elf32_body_big_endian, + )), +) + + +if __name__ == "__main__": + obj = elf32_file.parse_stream(open("../../../tests/_ctypes_test.so", "rb")) + #[s.data.value for s in obj.sections] + print(obj) + + + diff --git a/external/construct/formats/executable/pe32.py b/external/construct/formats/executable/pe32.py new file mode 100644 index 0000000..1463ec3 --- /dev/null +++ b/external/construct/formats/executable/pe32.py @@ -0,0 +1,420 @@ +""" +Portable Executable (PE) 32 bit, little endian +Used on MSWindows systems (including DOS) for EXEs and DLLs + +1999 paper: +http://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/pecoff.doc + +2006 with updates relevant for .NET: +http://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/pecoff_v8.doc +""" +from construct import * +import time +import six + + +class UTCTimeStampAdapter(Adapter): + def _decode(self, obj, context): + return time.ctime(obj) + def _encode(self, obj, context): + return int(time.mktime(time.strptime(obj))) + +def UTCTimeStamp(name): + return UTCTimeStampAdapter(ULInt32(name)) + +class NamedSequence(Adapter): + """ + creates a mapping between the elements of a sequence and their respective + names. this is useful for sequences of a variable length, where each + element in the sequence has a name (as is the case with the data + directories of the PE header) + """ + __slots__ = ["mapping", "rev_mapping"] + prefix = "unnamed_" + def __init__(self, subcon, mapping): + Adapter.__init__(self, subcon) + self.mapping = mapping + self.rev_mapping = dict((v, k) for k, v in mapping.items()) + def _encode(self, obj, context): + d = obj.__dict__ + obj2 = [None] * len(d) + for name, value in d.items(): + if name in self.rev_mapping: + index = self.rev_mapping[name] + elif name.startswith("__"): + obj2.pop(-1) + continue + elif name.startswith(self.prefix): + index = int(name.split(self.prefix)[1]) + else: + raise ValueError("no mapping defined for %r" % (name,)) + obj2[index] = value + return obj2 + def _decode(self, obj, context): + obj2 = Container() + for i, item in enumerate(obj): + if i in self.mapping: + name = self.mapping[i] + else: + name = "%s%d" % (self.prefix, i) + setattr(obj2, name, item) + return obj2 + + +msdos_header = Struct("msdos_header", + Magic("MZ"), + ULInt16("partPag"), + ULInt16("page_count"), + ULInt16("relocation_count"), + ULInt16("header_size"), + ULInt16("minmem"), + ULInt16("maxmem"), + ULInt16("relocation_stackseg"), + ULInt16("exe_stackptr"), + ULInt16("checksum"), + ULInt16("exe_ip"), + ULInt16("relocation_codeseg"), + ULInt16("table_offset"), + ULInt16("overlay"), + Padding(8), + ULInt16("oem_id"), + ULInt16("oem_info"), + Padding(20), + ULInt32("coff_header_pointer"), + Anchor("_assembly_start"), + OnDemand( + HexDumpAdapter( + Field("code", + lambda ctx: ctx.coff_header_pointer - ctx._assembly_start + ) + ) + ), +) + +symbol_table = Struct("symbol_table", + String("name", 8, padchar = six.b("\x00")), + ULInt32("value"), + Enum(ExprAdapter(SLInt16("section_number"), + encoder = lambda obj, ctx: obj + 1, + decoder = lambda obj, ctx: obj - 1, + ), + UNDEFINED = -1, + ABSOLUTE = -2, + DEBUG = -3, + _default_ = Pass, + ), + Enum(ULInt8("complex_type"), + NULL = 0, + POINTER = 1, + FUNCTION = 2, + ARRAY = 3, + ), + Enum(ULInt8("base_type"), + NULL = 0, + VOID = 1, + CHAR = 2, + SHORT = 3, + INT = 4, + LONG = 5, + FLOAT = 6, + DOUBLE = 7, + STRUCT = 8, + UNION = 9, + ENUM = 10, + MOE = 11, + BYTE = 12, + WORD = 13, + UINT = 14, + DWORD = 15, + ), + Enum(ULInt8("storage_class"), + END_OF_FUNCTION = 255, + NULL = 0, + AUTOMATIC = 1, + EXTERNAL = 2, + STATIC = 3, + REGISTER = 4, + EXTERNAL_DEF = 5, + LABEL = 6, + UNDEFINED_LABEL = 7, + MEMBER_OF_STRUCT = 8, + ARGUMENT = 9, + STRUCT_TAG = 10, + MEMBER_OF_UNION = 11, + UNION_TAG = 12, + TYPE_DEFINITION = 13, + UNDEFINED_STATIC = 14, + ENUM_TAG = 15, + MEMBER_OF_ENUM = 16, + REGISTER_PARAM = 17, + BIT_FIELD = 18, + BLOCK = 100, + FUNCTION = 101, + END_OF_STRUCT = 102, + FILE = 103, + SECTION = 104, + WEAK_EXTERNAL = 105, + ), + ULInt8("number_of_aux_symbols"), + Array(lambda ctx: ctx.number_of_aux_symbols, + Bytes("aux_symbols", 18) + ) +) + +coff_header = Struct("coff_header", + Magic("PE\x00\x00"), + Enum(ULInt16("machine_type"), + UNKNOWN = 0x0, + AM33 = 0x1d3, + AMD64 = 0x8664, + ARM = 0x1c0, + EBC = 0xebc, + I386 = 0x14c, + IA64 = 0x200, + M32R = 0x9041, + MIPS16 = 0x266, + MIPSFPU = 0x366, + MIPSFPU16 = 0x466, + POWERPC = 0x1f0, + POWERPCFP = 0x1f1, + R4000 = 0x166, + SH3 = 0x1a2, + SH3DSP = 0x1a3, + SH4 = 0x1a6, + SH5= 0x1a8, + THUMB = 0x1c2, + WCEMIPSV2 = 0x169, + _default_ = Pass + ), + ULInt16("number_of_sections"), + UTCTimeStamp("time_stamp"), + ULInt32("symbol_table_pointer"), + ULInt32("number_of_symbols"), + ULInt16("optional_header_size"), + FlagsEnum(ULInt16("characteristics"), + RELOCS_STRIPPED = 0x0001, + EXECUTABLE_IMAGE = 0x0002, + LINE_NUMS_STRIPPED = 0x0004, + LOCAL_SYMS_STRIPPED = 0x0008, + AGGRESSIVE_WS_TRIM = 0x0010, + LARGE_ADDRESS_AWARE = 0x0020, + MACHINE_16BIT = 0x0040, + BYTES_REVERSED_LO = 0x0080, + MACHINE_32BIT = 0x0100, + DEBUG_STRIPPED = 0x0200, + REMOVABLE_RUN_FROM_SWAP = 0x0400, + SYSTEM = 0x1000, + DLL = 0x2000, + UNIPROCESSOR_ONLY = 0x4000, + BIG_ENDIAN_MACHINE = 0x8000, + ), + + # symbol table + Pointer(lambda ctx: ctx.symbol_table_pointer, + Array(lambda ctx: ctx.number_of_symbols, symbol_table) + ) +) + +def PEPlusField(name): + return IfThenElse(name, lambda ctx: ctx.pe_type == "PE32_plus", + ULInt64(None), + ULInt32(None), + ) + +optional_header = Struct("optional_header", + # standard fields + Enum(ULInt16("pe_type"), + PE32 = 0x10b, + PE32_plus = 0x20b, + ), + ULInt8("major_linker_version"), + ULInt8("minor_linker_version"), + ULInt32("code_size"), + ULInt32("initialized_data_size"), + ULInt32("uninitialized_data_size"), + ULInt32("entry_point_pointer"), + ULInt32("base_of_code"), + + # only in PE32 files + If(lambda ctx: ctx.pe_type == "PE32", + ULInt32("base_of_data") + ), + + # WinNT-specific fields + PEPlusField("image_base"), + ULInt32("section_aligment"), + ULInt32("file_alignment"), + ULInt16("major_os_version"), + ULInt16("minor_os_version"), + ULInt16("major_image_version"), + ULInt16("minor_image_version"), + ULInt16("major_subsystem_version"), + ULInt16("minor_subsystem_version"), + Padding(4), + ULInt32("image_size"), + ULInt32("headers_size"), + ULInt32("checksum"), + Enum(ULInt16("subsystem"), + UNKNOWN = 0, + NATIVE = 1, + WINDOWS_GUI = 2, + WINDOWS_CUI = 3, + POSIX_CIU = 7, + WINDOWS_CE_GUI = 9, + EFI_APPLICATION = 10, + EFI_BOOT_SERVICE_DRIVER = 11, + EFI_RUNTIME_DRIVER = 12, + EFI_ROM = 13, + XBOX = 14, + _default_ = Pass + ), + FlagsEnum(ULInt16("dll_characteristics"), + NO_BIND = 0x0800, + WDM_DRIVER = 0x2000, + TERMINAL_SERVER_AWARE = 0x8000, + ), + PEPlusField("reserved_stack_size"), + PEPlusField("stack_commit_size"), + PEPlusField("reserved_heap_size"), + PEPlusField("heap_commit_size"), + ULInt32("loader_flags"), + ULInt32("number_of_data_directories"), + + NamedSequence( + Array(lambda ctx: ctx.number_of_data_directories, + Struct("data_directories", + ULInt32("address"), + ULInt32("size"), + ) + ), + mapping = { + 0 : 'export_table', + 1 : 'import_table', + 2 : 'resource_table', + 3 : 'exception_table', + 4 : 'certificate_table', + 5 : 'base_relocation_table', + 6 : 'debug', + 7 : 'architecture', + 8 : 'global_ptr', + 9 : 'tls_table', + 10 : 'load_config_table', + 11 : 'bound_import', + 12 : 'import_address_table', + 13 : 'delay_import_descriptor', + 14 : 'complus_runtime_header', + } + ), +) + +section = Struct("section", + String("name", 8, padchar = six.b("\x00")), + ULInt32("virtual_size"), + ULInt32("virtual_address"), + ULInt32("raw_data_size"), + ULInt32("raw_data_pointer"), + ULInt32("relocations_pointer"), + ULInt32("line_numbers_pointer"), + ULInt16("number_of_relocations"), + ULInt16("number_of_line_numbers"), + FlagsEnum(ULInt32("characteristics"), + TYPE_REG = 0x00000000, + TYPE_DSECT = 0x00000001, + TYPE_NOLOAD = 0x00000002, + TYPE_GROUP = 0x00000004, + TYPE_NO_PAD = 0x00000008, + TYPE_COPY = 0x00000010, + CNT_CODE = 0x00000020, + CNT_INITIALIZED_DATA = 0x00000040, + CNT_UNINITIALIZED_DATA = 0x00000080, + LNK_OTHER = 0x00000100, + LNK_INFO = 0x00000200, + TYPE_OVER = 0x00000400, + LNK_REMOVE = 0x00000800, + LNK_COMDAT = 0x00001000, + MEM_FARDATA = 0x00008000, + MEM_PURGEABLE = 0x00020000, + MEM_16BIT = 0x00020000, + MEM_LOCKED = 0x00040000, + MEM_PRELOAD = 0x00080000, + ALIGN_1BYTES = 0x00100000, + ALIGN_2BYTES = 0x00200000, + ALIGN_4BYTES = 0x00300000, + ALIGN_8BYTES = 0x00400000, + ALIGN_16BYTES = 0x00500000, + ALIGN_32BYTES = 0x00600000, + ALIGN_64BYTES = 0x00700000, + ALIGN_128BYTES = 0x00800000, + ALIGN_256BYTES = 0x00900000, + ALIGN_512BYTES = 0x00A00000, + ALIGN_1024BYTES = 0x00B00000, + ALIGN_2048BYTES = 0x00C00000, + ALIGN_4096BYTES = 0x00D00000, + ALIGN_8192BYTES = 0x00E00000, + LNK_NRELOC_OVFL = 0x01000000, + MEM_DISCARDABLE = 0x02000000, + MEM_NOT_CACHED = 0x04000000, + MEM_NOT_PAGED = 0x08000000, + MEM_SHARED = 0x10000000, + MEM_EXECUTE = 0x20000000, + MEM_READ = 0x40000000, + MEM_WRITE = 0x80000000, + ), + + OnDemandPointer(lambda ctx: ctx.raw_data_pointer, + HexDumpAdapter(Field("raw_data", lambda ctx: ctx.raw_data_size)) + ), + + OnDemandPointer(lambda ctx: ctx.line_numbers_pointer, + Array(lambda ctx: ctx.number_of_line_numbers, + Struct("line_numbers", + ULInt32("type"), + ULInt16("line_number"), + ) + ) + ), + + OnDemandPointer(lambda ctx: ctx.relocations_pointer, + Array(lambda ctx: ctx.number_of_relocations, + Struct("relocations", + ULInt32("virtual_address"), + ULInt32("symbol_table_index"), + ULInt16("type"), + ) + ) + ), +) + +pe32_file = Struct("pe32_file", + # headers + msdos_header, + coff_header, + Anchor("_start_of_optional_header"), + optional_header, + Anchor("_end_of_optional_header"), + Padding(lambda ctx: min(0, + ctx.coff_header.optional_header_size - + ctx._end_of_optional_header + + ctx._start_of_optional_header + ) + ), + + # sections + Array(lambda ctx: ctx.coff_header.number_of_sections, section) +) + + +if __name__ == "__main__": + print (pe32_file.parse_stream(open("../../../tests/NOTEPAD.EXE", "rb"))) + print (pe32_file.parse_stream(open("../../../tests/sqlite3.dll", "rb"))) + + + + + + + + + + + diff --git a/external/construct/formats/filesystem/__init__.py b/external/construct/formats/filesystem/__init__.py new file mode 100644 index 0000000..217ec83 --- /dev/null +++ b/external/construct/formats/filesystem/__init__.py @@ -0,0 +1,4 @@ +""" +file systems on-disk formats (ext2, fat32, ntfs, ...) +and related disk formats (mbr, ...) +""" diff --git a/external/construct/formats/filesystem/ext2.py b/external/construct/formats/filesystem/ext2.py new file mode 100644 index 0000000..954049e --- /dev/null +++ b/external/construct/formats/filesystem/ext2.py @@ -0,0 +1,157 @@ +""" +Extension 2 (ext2) +Used in Linux systems +""" +from construct import * + + +Char = SLInt8 +UChar = ULInt8 +Short = SLInt16 +UShort = ULInt16 +Long = SLInt32 +ULong = ULInt32 + +def BlockPointer(name): + return Struct(name, + ULong("block_number"), + OnDemandPointer(lambda ctx: ctx["block_number"]), + ) + +superblock = Struct("superblock", + ULong('inodes_count'), + ULong('blocks_count'), + ULong('reserved_blocks_count'), + ULong('free_blocks_count'), + ULong('free_inodes_count'), + ULong('first_data_block'), + Enum(ULong('log_block_size'), + OneKB = 0, + TwoKB = 1, + FourKB = 2, + ), + Long('log_frag_size'), + ULong('blocks_per_group'), + ULong('frags_per_group'), + ULong('inodes_per_group'), + ULong('mtime'), + ULong('wtime'), + UShort('mnt_count'), + Short('max_mnt_count'), + Const(UShort('magic'), 0xEF53), + UShort('state'), + UShort('errors'), + Padding(2), + ULong('lastcheck'), + ULong('checkinterval'), + ULong('creator_os'), + ULong('rev_level'), + Padding(235 * 4), +) + +group_descriptor = Struct("group_descriptor", + ULong('block_bitmap'), + ULong('inode_bitmap'), + ULong('inode_table'), + UShort('free_blocks_count'), + UShort('free_inodes_count'), + UShort('used_dirs_count'), + Padding(14), +) + +inode = Struct("inode", + FlagsEnum(UShort('mode'), + IXOTH = 0x0001, + IWOTH = 0x0002, + IROTH = 0x0004, + IRWXO = 0x0007, + IXGRP = 0x0008, + IWGRP = 0x0010, + IRGRP = 0x0020, + IRWXG = 0x0038, + IXUSR = 0x0040, + IWUSR = 0x0080, + IRUSR = 0x0100, + IRWXU = 0x01C0, + ISVTX = 0x0200, + ISGID = 0x0400, + ISUID = 0x0800, + IFIFO = 0x1000, + IFCHR = 0x2000, + IFDIR = 0x4000, + IFBLK = 0x6000, + IFREG = 0x8000, + IFLNK = 0xC000, + IFSOCK = 0xA000, + IFMT = 0xF000, + ), + UShort('uid'), + ULong('size'), + ULong('atime'), + ULong('ctime'), + ULong('mtime'), + ULong('dtime'), + UShort('gid'), + UShort('links_count'), + ULong('blocks'), + FlagsEnum(ULong('flags'), + SecureDelete = 0x0001, + AllowUndelete = 0x0002, + Compressed = 0x0004, + Synchronous = 0x0008, + ), + Padding(4), + Array(12, ULong('blocks')), + ULong("indirect1_block"), + ULong("indirect2_block"), + ULong("indirect3_block"), + ULong('version'), + ULong('file_acl'), + ULong('dir_acl'), + ULong('faddr'), + UChar('frag'), + Byte('fsize'), + Padding(10) , +) + +# special inodes +EXT2_BAD_INO = 1 +EXT2_ROOT_INO = 2 +EXT2_ACL_IDX_INO = 3 +EXT2_ACL_DATA_INO = 4 +EXT2_BOOT_LOADER_INO = 5 +EXT2_UNDEL_DIR_INO = 6 +EXT2_FIRST_INO = 11 + +directory_record = Struct("directory_entry", + ULong("inode"), + UShort("rec_length"), + UShort("name_length"), + Field("name", lambda ctx: ctx["name_length"]), + Padding(lambda ctx: ctx["rec_length"] - ctx["name_length"]) +) + +if __name__ == "__main__": + print (superblock.sizeof()) + + + + + + + + + + + + + + + + + + + + + + diff --git a/external/construct/formats/filesystem/fat16.py b/external/construct/formats/filesystem/fat16.py new file mode 100644 index 0000000..5d6caf1 --- /dev/null +++ b/external/construct/formats/filesystem/fat16.py @@ -0,0 +1,226 @@ +# fat.py; ad-hoc fat16 reader +# by Bram Westerbaan <bram@westerbaan.name> +# +# references: +# http://en.wikipedia.org/wiki/File_Allocation_Table +# http://www.ecma-international.org/publications/standards/Ecma-107.htm +# +# example: +# with open("/dev/sdc1") as file: +# fs = FatFs(file) +# for rootdir in fs: +# print rootdir +import numbers +from io import BytesIO, BufferedReader +from construct import Struct, Byte, Bytes, ULInt16, ULInt32, Enum, \ + Array, Padding, Embed, Pass, BitStruct, Flag, Const + + +def Fat16Header(name): + return Struct(name, + Bytes("jumpInstruction", 3), + Bytes("creatingSystemId", 8), + ULInt16("sectorSize"), + Byte("sectorsPerCluster"), + ULInt16("reservedSectorCount"), + Byte("fatCount"), + ULInt16("rootdirEntryCount"), + ULInt16("sectorCount_small"), + Byte("mediaId"), + ULInt16("sectorsPerFat"), + ULInt16("sectorsPerTrack"), + ULInt16("sideCount"), + ULInt32("hiddenSectorCount"), + ULInt32("sectorCount_large"), + Byte("physicalDriveNumber"), + Byte("currentHead"), + Byte("extendedBootSignature"), + Bytes("volumeId", 4), + Bytes("volumeLabel", 11), + Const(Bytes("fsType", 8), "FAT16 "), + Bytes("bootCode", 448), + Const(Bytes("bootSectorSignature", 2), "\x55\xaa")) + +def BootSector(name): + header = Fat16Header("header") + return Struct(name, + Embed(header), + Padding(lambda ctx: ctx.sectorSize - header.sizeof())) + +def FatEntry(name): + return Enum(ULInt16(name), + free_cluster = 0x0000, + bad_cluster = 0xfff7, + last_cluster = 0xffff, + _default_ = Pass) + +def DirEntry(name): + return Struct(name, + Bytes("name", 8), + Bytes("extension", 3), + BitStruct("attributes", + Flag("unused"), + Flag("device"), + Flag("archive"), + Flag("subDirectory"), + Flag("volumeLabel"), + Flag("system"), + Flag("hidden"), + Flag("readonly")), + # reserved + Padding(10), + ULInt16("timeRecorded"), + ULInt16("dateRecorded"), + ULInt16("firstCluster"), + ULInt32("fileSize")) + +def PreDataRegion(name): + rde = DirEntry("rootdirs") + fe = FatEntry("fats") + return Struct(name, + Embed(BootSector("bootSector")), + # the remaining reserved sectors + Padding(lambda ctx: (ctx.reservedSectorCount - 1) + * ctx.sectorSize), + # file allocation tables + Array(lambda ctx: (ctx.fatCount), + Array(lambda ctx: ctx.sectorsPerFat * + ctx.sectorSize / fe.sizeof(), fe)), + # root directories + Array(lambda ctx: (ctx.rootdirEntryCount*rde.sizeof()) + / ctx.sectorSize, rde)) + +class File(object): + def __init__(self, dirEntry, fs): + self.fs = fs + self.dirEntry = dirEntry + + @classmethod + def fromDirEntry(cls, dirEntry, fs): + if dirEntry.name[0] in "\x00\xe5\x2e": + return None + a = dirEntry.attributes + #Long file name directory entry + if a.volumeLabel and a.system and a.hidden and a.readonly: + return None + if a.subDirectory: + return Directory(dirEntry, fs) + return File(dirEntry, fs) + + @classmethod + def fromDirEntries(cls, dirEntries, fs): + return filter(None, [cls.fromDirEntry(de, fs) + for de in dirEntries]) + + def toStream(self, stream): + self.fs.fileToStream(self.dirEntry.firstCluster, stream) + + @property + def name(self): + return "%s.%s" % (self.dirEntry.name.rstrip(), + self.dirEntry.extension) + + def __str__(self): + return "&%s %s" % (self.dirEntry.firstCluster, self.name) + +class Directory(File): + def __init__(self, dirEntry, fs, children=None): + File.__init__(self, dirEntry, fs) + self.children = children + if not self.children: + self.children = File.fromDirEntries(\ + self.fs.getDirEntries(\ + self.dirEntry.firstCluster), fs) + + @property + def name(self): + return self.dirEntry.name.rstrip() + + def __str__(self): + return "&%s %s/" % (self.dirEntry.firstCluster, self.name) + + def __getitem__(self, name): + for file in self.children: + if file.name == name: + return file + + def __iter__(self): + return iter(self.children) + +class FatFs(Directory): + def __init__(self, stream): + self.stream = stream + self.pdr = PreDataRegion("pdr").parse_stream(stream) + Directory.__init__(self, dirEntry = None, + fs = self, children = File.fromDirEntries( + self.pdr.rootdirs, self)) + + def fileToStream(self, clidx, stream): + for clidx in self.getLinkedClusters(clidx): + self.clusterToStream(clidx, stream) + + def clusterToStream(self, clidx, stream): + start, todo = self.getClusterSlice(clidx) + self.stream.seek(start) + while todo > 0: + read = self.stream.read(todo) + if not len(read): + print("failed to read %s bytes at %s" % (todo, self.stream.tell())) + raise EOFError() + todo -= len(read) + stream.write(read) + + def getClusterSlice(self, clidx): + startSector = self.pdr.reservedSectorCount \ + + self.pdr.fatCount * self.pdr.sectorsPerFat \ + + (self.pdr.rootdirEntryCount * 32) \ + / self.pdr.sectorSize \ + + (clidx-2) * self.pdr.sectorsPerCluster + start = startSector * self.pdr.sectorSize + length = self.pdr.sectorSize * self.pdr.sectorsPerCluster + return (start, length) + + def getLinkedClusters(self, clidx): + res = [] + while clidx != "last_cluster": + if not isinstance(clidx, numbers.Real): + print(clidx) + assert False + assert 2 <= clidx <= 0xffef + res.append(clidx) + clidx = self.getNextCluster(clidx) + assert clidx not in res + return res + + def getNextCluster(self, clidx): + ress = set([fat[clidx] for fat in self.pdr.fats]) + if len(ress)==1: + return ress.pop() + print("inconsistencie between FATs: %s points to" % clidx) + for i,fat in enumerate(self.pdr.fats): + print("\t%s according to fat #%s" % (fat[clidx], i)) + res = ress.pop() + print ("assuming %s" % res) + return res + + def getDirEntries(self, clidx): + try: + for de in self._getDirEntries(clidx): + yield de + except IOError: + print("failed to read directory entries at %s" % clidx) + + def _getDirEntries(self, clidx): + de = DirEntry("dirEntry") + with BytesIO() as mem: + self.fileToStream(clidx, mem) + mem.seek(0) + with BufferedReader(mem) as reader: + while reader.peek(1): + yield de.parse_stream(reader) + def __str__(self): + return "/" + + @property + def name(self): + return "" diff --git a/external/construct/formats/filesystem/mbr.py b/external/construct/formats/filesystem/mbr.py new file mode 100644 index 0000000..1fd5a62 --- /dev/null +++ b/external/construct/formats/filesystem/mbr.py @@ -0,0 +1,77 @@ +""" +Master Boot Record +The first sector on disk, contains the partition table, bootloader, et al. + +http://www.win.tue.nl/~aeb/partitions/partition_types-1.html +""" +from construct import * +from binascii import unhexlify +import six + + +mbr = Struct("mbr", + HexDumpAdapter(Bytes("bootloader_code", 446)), + Array(4, + Struct("partitions", + Enum(Byte("state"), + INACTIVE = 0x00, + ACTIVE = 0x80, + ), + BitStruct("beginning", + Octet("head"), + Bits("sect", 6), + Bits("cyl", 10), + ), + Enum(UBInt8("type"), + Nothing = 0x00, + FAT12 = 0x01, + XENIX_ROOT = 0x02, + XENIX_USR = 0x03, + FAT16_old = 0x04, + Extended_DOS = 0x05, + FAT16 = 0x06, + FAT32 = 0x0b, + FAT32_LBA = 0x0c, + NTFS = 0x07, + LINUX_SWAP = 0x82, + LINUX_NATIVE = 0x83, + _default_ = Pass, + ), + BitStruct("ending", + Octet("head"), + Bits("sect", 6), + Bits("cyl", 10), + ), + UBInt32("sector_offset"), # offset from MBR in sectors + UBInt32("size"), # in sectors + ) + ), + Const(Bytes("signature", 2), six.b("\x55\xAA")), +) + + + +if __name__ == "__main__": + cap1 = unhexlify(six.b( + "33C08ED0BC007CFB5007501FFCBE1B7CBF1B065057B9E501F3A4CBBDBE07B104386E00" + "7C09751383C510E2F4CD188BF583C610497419382C74F6A0B507B4078BF0AC3C0074FC" + "BB0700B40ECD10EBF2884E10E84600732AFE4610807E040B740B807E040C7405A0B607" + "75D2804602068346080683560A00E821007305A0B607EBBC813EFE7D55AA740B807E10" + "0074C8A0B707EBA98BFC1E578BF5CBBF05008A5600B408CD1372238AC1243F988ADE8A" + "FC43F7E38BD186D6B106D2EE42F7E239560A77237205394608731CB80102BB007C8B4E" + "028B5600CD1373514F744E32E48A5600CD13EBE48A560060BBAA55B441CD13723681FB" + "55AA7530F6C101742B61606A006A00FF760AFF76086A0068007C6A016A10B4428BF4CD" + "136161730E4F740B32E48A5600CD13EBD661F9C3496E76616C69642070617274697469" + "6F6E207461626C65004572726F72206C6F6164696E67206F7065726174696E67207379" + "7374656D004D697373696E67206F7065726174696E672073797374656D000000000000" + "0000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000002C4463B7BDB7BD00008001010007FEFFFF3F" + "000000371671020000C1FF0FFEFFFF761671028A8FDF06000000000000000000000000" + "000000000000000000000000000000000000000055AA")) + + print(mbr.parse(cap1)) + + + + + diff --git a/external/construct/formats/graphics/__init__.py b/external/construct/formats/graphics/__init__.py new file mode 100644 index 0000000..4abda02 --- /dev/null +++ b/external/construct/formats/graphics/__init__.py @@ -0,0 +1,4 @@ +""" +graphic file formats, including imagery (bmp, jpg, gif, png, ...), +models (3ds, ...), etc. +""" diff --git a/external/construct/formats/graphics/bmp.py b/external/construct/formats/graphics/bmp.py new file mode 100644 index 0000000..abe1ad0 --- /dev/null +++ b/external/construct/formats/graphics/bmp.py @@ -0,0 +1,113 @@ +""" +Windows/OS2 Bitmap (BMP) +this could have been a perfect show-case file format, but they had to make +it ugly (all sorts of alignment or +""" +from construct import * + + +#=============================================================================== +# pixels: uncompressed +#=============================================================================== +def UncompressedRows(subcon, align_to_byte = False): + """argh! lines must be aligned to a 4-byte boundary, and bit-pixel + lines must be aligned to full bytes...""" + if align_to_byte: + line_pixels = Bitwise( + Aligned(Array(lambda ctx: ctx.width, subcon), modulus = 8) + ) + else: + line_pixels = Array(lambda ctx: ctx.width, subcon) + return Array(lambda ctx: ctx.height, + Aligned(line_pixels, modulus = 4) + ) + +uncompressed_pixels = Switch("uncompressed", lambda ctx: ctx.bpp, + { + 1 : UncompressedRows(Bit("index"), align_to_byte = True), + 4 : UncompressedRows(Nibble("index"), align_to_byte = True), + 8 : UncompressedRows(Byte("index")), + 24 : UncompressedRows( + Sequence("rgb", Byte("red"), Byte("green"), Byte("blue")) + ), + } +) + +#=============================================================================== +# pixels: Run Length Encoding (RLE) 8 bit +#=============================================================================== +class RunLengthAdapter(Adapter): + def _encode(self, obj): + return len(obj), obj[0] + def _decode(self, obj): + length, value = obj + return [value] * length + +rle8pixel = RunLengthAdapter( + Sequence("rle8pixel", + Byte("length"), + Byte("value") + ) +) + +#=============================================================================== +# file structure +#=============================================================================== +bitmap_file = Struct("bitmap_file", + # header + Const(String("signature", 2), "BM"), + ULInt32("file_size"), + Padding(4), + ULInt32("data_offset"), + ULInt32("header_size"), + Enum(Alias("version", "header_size"), + v2 = 12, + v3 = 40, + v4 = 108, + ), + ULInt32("width"), + ULInt32("height"), + Value("number_of_pixels", lambda ctx: ctx.width * ctx.height), + ULInt16("planes"), + ULInt16("bpp"), # bits per pixel + Enum(ULInt32("compression"), + Uncompressed = 0, + RLE8 = 1, + RLE4 = 2, + Bitfields = 3, + JPEG = 4, + PNG = 5, + ), + ULInt32("image_data_size"), # in bytes + ULInt32("horizontal_dpi"), + ULInt32("vertical_dpi"), + ULInt32("colors_used"), + ULInt32("important_colors"), + + # palette (24 bit has no palette) + OnDemand( + Array(lambda ctx: 2 ** ctx.bpp if ctx.bpp <= 8 else 0, + Struct("palette", + Byte("blue"), + Byte("green"), + Byte("red"), + Padding(1), + ) + ) + ), + + # pixels + OnDemandPointer(lambda ctx: ctx.data_offset, + Switch("pixels", lambda ctx: ctx.compression, + { + "Uncompressed" : uncompressed_pixels, + } + ), + ), +) + + +if __name__ == "__main__": + obj = bitmap_file.parse_stream(open("../../../tests/bitmap8.bmp", "rb")) + print (obj) + print (repr(obj.pixels.value)) diff --git a/external/construct/formats/graphics/emf.py b/external/construct/formats/graphics/emf.py new file mode 100644 index 0000000..4f00a03 --- /dev/null +++ b/external/construct/formats/graphics/emf.py @@ -0,0 +1,198 @@ +""" +Enhanced Meta File +""" +from construct import * + + +record_type = Enum(ULInt32("record_type"), + ABORTPATH = 68, + ANGLEARC = 41, + ARC = 45, + ARCTO = 55, + BEGINPATH = 59, + BITBLT = 76, + CHORD = 46, + CLOSEFIGURE = 61, + CREATEBRUSHINDIRECT = 39, + CREATEDIBPATTERNBRUSHPT = 94, + CREATEMONOBRUSH = 93, + CREATEPALETTE = 49, + CREATEPEN = 38, + DELETEOBJECT = 40, + ELLIPSE = 42, + ENDPATH = 60, + EOF = 14, + EXCLUDECLIPRECT = 29, + EXTCREATEFONTINDIRECTW = 82, + EXTCREATEPEN = 95, + EXTFLOODFILL = 53, + EXTSELECTCLIPRGN = 75, + EXTTEXTOUTA = 83, + EXTTEXTOUTW = 84, + FILLPATH = 62, + FILLRGN = 71, + FLATTENPATH = 65, + FRAMERGN = 72, + GDICOMMENT = 70, + HEADER = 1, + INTERSECTCLIPRECT = 30, + INVERTRGN = 73, + LINETO = 54, + MASKBLT = 78, + MODIFYWORLDTRANSFORM = 36, + MOVETOEX = 27, + OFFSETCLIPRGN = 26, + PAINTRGN = 74, + PIE = 47, + PLGBLT = 79, + POLYBEZIER = 2, + POLYBEZIER16 = 85, + POLYBEZIERTO = 5, + POLYBEZIERTO16 = 88, + POLYDRAW = 56, + POLYDRAW16 = 92, + POLYGON = 3, + POLYGON16 = 86, + POLYLINE = 4, + POLYLINE16 = 87, + POLYLINETO = 6, + POLYLINETO16 = 89, + POLYPOLYGON = 8, + POLYPOLYGON16 = 91, + POLYPOLYLINE = 7, + POLYPOLYLINE16 = 90, + POLYTEXTOUTA = 96, + POLYTEXTOUTW = 97, + REALIZEPALETTE = 52, + RECTANGLE = 43, + RESIZEPALETTE = 51, + RESTOREDC = 34, + ROUNDRECT = 44, + SAVEDC = 33, + SCALEVIEWPORTEXTEX = 31, + SCALEWINDOWEXTEX = 32, + SELECTCLIPPATH = 67, + SELECTOBJECT = 37, + SELECTPALETTE = 48, + SETARCDIRECTION = 57, + SETBKCOLOR = 25, + SETBKMODE = 18, + SETBRUSHORGEX = 13, + SETCOLORADJUSTMENT = 23, + SETDIBITSTODEVICE = 80, + SETMAPMODE = 17, + SETMAPPERFLAGS = 16, + SETMETARGN = 28, + SETMITERLIMIT = 58, + SETPALETTEENTRIES = 50, + SETPIXELV = 15, + SETPOLYFILLMODE = 19, + SETROP2 = 20, + SETSTRETCHBLTMODE = 21, + SETTEXTALIGN = 22, + SETTEXTCOLOR = 24, + SETVIEWPORTEXTEX = 11, + SETVIEWPORTORGEX = 12, + SETWINDOWEXTEX = 9, + SETWINDOWORGEX = 10, + SETWORLDTRANSFORM = 35, + STRETCHBLT = 77, + STRETCHDIBITS = 81, + STROKEANDFILLPATH = 63, + STROKEPATH = 64, + WIDENPATH = 66, + _default_ = Pass, +) + +generic_record = Struct("records", + record_type, + ULInt32("record_size"), # Size of the record in bytes + Union("params", # Parameters + Field("raw", lambda ctx: ctx._.record_size - 8), + Array(lambda ctx: (ctx._.record_size - 8) // 4, ULInt32("params")) + ), +) + +header_record = Struct("header_record", + Const(record_type, "HEADER"), + ULInt32("record_size"), # Size of the record in bytes + SLInt32("bounds_left"), # Left inclusive bounds + SLInt32("bounds_right"), # Right inclusive bounds + SLInt32("bounds_top"), # Top inclusive bounds + SLInt32("bounds_bottom"), # Bottom inclusive bounds + SLInt32("frame_left"), # Left side of inclusive picture frame + SLInt32("frame_right"), # Right side of inclusive picture frame + SLInt32("frame_top"), # Top side of inclusive picture frame + SLInt32("frame_bottom"), # Bottom side of inclusive picture frame + Const(ULInt32("signature"), 0x464D4520), + ULInt32("version"), # Version of the metafile + ULInt32("size"), # Size of the metafile in bytes + ULInt32("num_of_records"), # Number of records in the metafile + ULInt16("num_of_handles"), # Number of handles in the handle table + Padding(2), + ULInt32("description_size"), # Size of description string in WORDs + ULInt32("description_offset"), # Offset of description string in metafile + ULInt32("num_of_palette_entries"), # Number of color palette entries + SLInt32("device_width_pixels"), # Width of reference device in pixels + SLInt32("device_height_pixels"), # Height of reference device in pixels + SLInt32("device_width_mm"), # Width of reference device in millimeters + SLInt32("device_height_mm"), # Height of reference device in millimeters + + # description string + Pointer(lambda ctx: ctx.description_offset, + StringAdapter( + Array(lambda ctx: ctx.description_size, + Field("description", 2) + ) + ) + ), + + # padding up to end of record + Padding(lambda ctx: ctx.record_size - 88), +) + +emf_file = Struct("emf_file", + header_record, + Array(lambda ctx: ctx.header_record.num_of_records - 1, + generic_record + ), +) + + +if __name__ == "__main__": + obj = emf_file.parse_stream(open("../../../tests/emf1.emf", "rb")) + print (obj) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/external/construct/formats/graphics/gif.py b/external/construct/formats/graphics/gif.py new file mode 100644 index 0000000..fa50150 --- /dev/null +++ b/external/construct/formats/graphics/gif.py @@ -0,0 +1,151 @@ +# Contributed by +# Dany Zatuchna (danzat at gmail) +""" Implementation of the following grammar for the GIF89a file format +<GIF Data Stream> ::= Header <Logical Screen> <Data>* Trailer + +<Logical Screen> ::= Logical Screen Descriptor [Global Color Table] + +<Data> ::= <Graphic Block> | + <Special-Purpose Block> + +<Graphic Block> ::= [Graphic Control Extension] <Graphic-Rendering Block> + +<Graphic-Rendering Block> ::= <Table-Based Image> | + Plain Text Extension + +<Table-Based Image> ::= Image Descriptor [Local Color Table] Image Data + +<Special-Purpose Block> ::= Application Extension | + Comment Extension +""" +from construct import * +import six + + +data_sub_block = Struct("data_sub_block", + ULInt8("size"), + String("data", lambda ctx: ctx["size"]) +) + +gif_logical_screen = Struct("logical_screen", + ULInt16("width"), + ULInt16("height"), + BitStruct("flags", + Bit("global_color_table"), + BitField("color_resolution", 3), + Bit("sort_flag"), + BitField("global_color_table_bpp", 3) + ), + ULInt8("bgcolor_index"), + ULInt8("pixel_aspect_ratio"), + If(lambda ctx: ctx["flags"]["global_color_table"], + Array(lambda ctx: 2**(ctx["flags"]["global_color_table_bpp"] + 1), + Struct("palette", + ULInt8("R"), + ULInt8("G"), + ULInt8("B") + ) + ) + ) +) + +gif_header = Struct("gif_header", + Const(String("signature", 3), six.b("GIF")), + Const(String("version", 3), six.b("89a")), +) + +application_extension = Struct("application_extension", + Const(ULInt8("block_size"), 11), + String("application_identifier", 8), + String("application_auth_code", 3), + data_sub_block, + ULInt8("block_terminator") +) + +comment_extension = Struct("comment_extension", + data_sub_block, + ULInt8("block_terminator") +) + +graphic_control_extension = Struct("graphic_control_extension", + Const(ULInt8("block_size"), 4), + BitStruct("flags", + BitField("reserved", 3), + BitField("disposal_method", 3), + Bit("user_input_flag"), + Bit("transparent_color_flag"), + ), + ULInt16("delay"), + ULInt8("transparent_color_index"), + ULInt8("block_terminator") +) + +plain_text_extension = Struct("plain_text_extension", + Const(ULInt8("block_size"), 12), + ULInt16("text_left"), + ULInt16("text_top"), + ULInt16("text_width"), + ULInt16("text_height"), + ULInt8("cell_width"), + ULInt8("cell_height"), + ULInt8("foreground_index"), + ULInt8("background_index"), + data_sub_block, + ULInt8("block_terminator") +) + +extension = Struct("extension", + ULInt8("label"), + Switch("ext", lambda ctx: ctx["label"], { + 0xFF: application_extension, + 0xFE: comment_extension, + 0xF9: graphic_control_extension, + 0x01: plain_text_extension + }) +) + +image_descriptor = Struct("image_descriptor", + ULInt16("left"), + ULInt16("top"), + ULInt16("width"), + ULInt16("height"), + BitStruct("flags", + Bit("local_color_table"), + Bit("interlace"), + Bit("sort"), + BitField("reserved", 2), + BitField("local_color_table_bpp", 3) + ), + If(lambda ctx: ctx["flags"]["local_color_table"], + Array(lambda ctx: 2**(ctx["flags"]["local_color_table_bpp"] + 1), + Struct("palette", + ULInt8("R"), + ULInt8("G"), + ULInt8("B") + ) + ) + ), + ULInt8("LZW_minimum_code_size"), + RepeatUntil(lambda obj, ctx: obj.size == 0, data_sub_block) +) + +gif_data = Struct("gif_data", + ULInt8("introducer"), + Switch("dat", lambda ctx: ctx["introducer"], { + 0x21: extension, + 0x2C: image_descriptor + }) +) + +gif_file = Struct("gif_file", + gif_header, + gif_logical_screen, + OptionalGreedyRange(gif_data), + #Const(ULInt8("trailer"), 0x3B) +) + +if __name__ == "__main__": + f = open("../../../tests/sample.gif", "rb") + s = f.read() + f.close() + print(gif_file.parse(s)) diff --git a/external/construct/formats/graphics/png.py b/external/construct/formats/graphics/png.py new file mode 100644 index 0000000..39edf3a --- /dev/null +++ b/external/construct/formats/graphics/png.py @@ -0,0 +1,355 @@ +""" +Portable Network Graphics (PNG) file format +Official spec: http://www.w3.org/TR/PNG + +Original code contributed by Robin Munn (rmunn at pobox dot com) +(although the code has been extensively reorganized to meet Construct's +coding conventions) +""" +from construct import * +import six + + +#=============================================================================== +# utils +#=============================================================================== +def Coord(name, field=UBInt8): + return Struct(name, + field("x"), + field("y"), + ) + +compression_method = Enum(UBInt8("compression_method"), + deflate = 0, + _default_ = Pass +) + + +#=============================================================================== +# 11.2.3: PLTE - Palette +#=============================================================================== +plte_info = Struct("plte_info", + Value("num_entries", lambda ctx: ctx._.length / 3), + Array(lambda ctx: ctx.num_entries, + Struct("palette_entries", + UBInt8("red"), + UBInt8("green"), + UBInt8("blue"), + ), + ), +) + +#=============================================================================== +# 11.2.4: IDAT - Image data +#=============================================================================== +idat_info = OnDemand( + Field("idat_info", lambda ctx: ctx.length), +) + +#=============================================================================== +# 11.3.2.1: tRNS - Transparency +#=============================================================================== +trns_info = Switch("trns_info", lambda ctx: ctx._.image_header.color_type, + { + "greyscale": Struct("data", + UBInt16("grey_sample") + ), + "truecolor": Struct("data", + UBInt16("red_sample"), + UBInt16("blue_sample"), + UBInt16("green_sample"), + ), + "indexed": Array(lambda ctx: ctx.length, + UBInt8("alpha"), + ), + } +) + +#=============================================================================== +# 11.3.3.1: cHRM - Primary chromacities and white point +#=============================================================================== +chrm_info = Struct("chrm_info", + Coord("white_point", UBInt32), + Coord("red", UBInt32), + Coord("green", UBInt32), + Coord("blue", UBInt32), +) + +#=============================================================================== +# 11.3.3.2: gAMA - Image gamma +#=============================================================================== +gama_info = Struct("gama_info", + UBInt32("gamma"), +) + +#=============================================================================== +# 11.3.3.3: iCCP - Embedded ICC profile +#=============================================================================== +iccp_info = Struct("iccp_info", + CString("name"), + compression_method, + Field("compressed_profile", + lambda ctx: ctx._.length - (len(ctx.name) + 2) + ), +) + +#=============================================================================== +# 11.3.3.4: sBIT - Significant bits +#=============================================================================== +sbit_info = Switch("sbit_info", lambda ctx: ctx._.image_header.color_type, + { + "greyscale": Struct("data", + UBInt8("significant_grey_bits"), + ), + "truecolor": Struct("data", + UBInt8("significant_red_bits"), + UBInt8("significant_green_bits"), + UBInt8("significant_blue_bits"), + ), + "indexed": Struct("data", + UBInt8("significant_red_bits"), + UBInt8("significant_green_bits"), + UBInt8("significant_blue_bits"), + ), + "greywithalpha": Struct("data", + UBInt8("significant_grey_bits"), + UBInt8("significant_alpha_bits"), + ), + "truewithalpha": Struct("data", + UBInt8("significant_red_bits"), + UBInt8("significant_green_bits"), + UBInt8("significant_blue_bits"), + UBInt8("significant_alpha_bits"), + ), + } +) + +#=============================================================================== +# 11.3.3.5: sRGB - Standard RPG color space +#=============================================================================== +srgb_info = Struct("srgb_info", + Enum(UBInt8("rendering_intent"), + perceptual = 0, + relative_colorimetric = 1, + saturation = 2, + absolute_colorimetric = 3, + _default_ = Pass, + ), +) + +#=============================================================================== +# 11.3.4.3: tEXt - Textual data +#=============================================================================== +text_info = Struct("text_info", + CString("keyword"), + Field("text", lambda ctx: ctx._.length - (len(ctx.keyword) + 1)), +) + +#=============================================================================== +# 11.3.4.4: zTXt - Compressed textual data +#=============================================================================== +ztxt_info = Struct("ztxt_info", + CString("keyword"), + compression_method, + OnDemand( + Field("compressed_text", + # As with iCCP, length is chunk length, minus length of + # keyword, minus two: one byte for the null terminator, + # and one byte for the compression method. + lambda ctx: ctx._.length - (len(ctx.keyword) + 2), + ), + ), +) + +#=============================================================================== +# 11.3.4.5: iTXt - International textual data +#=============================================================================== +itxt_info = Struct("itxt_info", + CString("keyword"), + UBInt8("compression_flag"), + compression_method, + CString("language_tag"), + CString("translated_keyword"), + OnDemand( + Field("text", + lambda ctx: ctx._.length - (len(ctx.keyword) + + len(ctx.language_tag) + len(ctx.translated_keyword) + 5), + ), + ), +) + +#=============================================================================== +# 11.3.5.1: bKGD - Background color +#=============================================================================== +bkgd_info = Switch("bkgd_info", lambda ctx: ctx._.image_header.color_type, + { + "greyscale": Struct("data", + UBInt16("background_greyscale_value"), + Alias("grey", "background_greyscale_value"), + ), + "greywithalpha": Struct("data", + UBInt16("background_greyscale_value"), + Alias("grey", "background_greyscale_value"), + ), + "truecolor": Struct("data", + UBInt16("background_red_value"), + UBInt16("background_green_value"), + UBInt16("background_blue_value"), + Alias("red", "background_red_value"), + Alias("green", "background_green_value"), + Alias("blue", "background_blue_value"), + ), + "truewithalpha": Struct("data", + UBInt16("background_red_value"), + UBInt16("background_green_value"), + UBInt16("background_blue_value"), + Alias("red", "background_red_value"), + Alias("green", "background_green_value"), + Alias("blue", "background_blue_value"), + ), + "indexed": Struct("data", + UBInt16("background_palette_index"), + Alias("index", "background_palette_index"), + ), + } +) + +#=============================================================================== +# 11.3.5.2: hIST - Image histogram +#=============================================================================== +hist_info = Array(lambda ctx: ctx._.length / 2, + UBInt16("frequency"), +) + +#=============================================================================== +# 11.3.5.3: pHYs - Physical pixel dimensions +#=============================================================================== +phys_info = Struct("phys_info", + UBInt32("pixels_per_unit_x"), + UBInt32("pixels_per_unit_y"), + Enum(UBInt8("unit"), + unknown = 0, + meter = 1, + _default_ = Pass + ), +) + +#=============================================================================== +# 11.3.5.4: sPLT - Suggested palette +#=============================================================================== +def splt_info_data_length(ctx): + if ctx.sample_depth == 8: + entry_size = 6 + else: + entry_size = 10 + return (ctx._.length - len(ctx.name) - 2) / entry_size + +splt_info = Struct("data", + CString("name"), + UBInt8("sample_depth"), + Array(lambda ctx: splt_info_data_length, + IfThenElse("table", lambda ctx: ctx.sample_depth == 8, + # Sample depth 8 + Struct("table", + UBInt8("red"), + UBInt8("green"), + UBInt8("blue"), + UBInt8("alpha"), + UBInt16("frequency"), + ), + # Sample depth 16 + Struct("table", + UBInt16("red"), + UBInt16("green"), + UBInt16("blue"), + UBInt16("alpha"), + UBInt16("frequency"), + ), + ), + ), +) + +#=============================================================================== +# 11.3.6.1: tIME - Image last-modification time +#=============================================================================== +time_info = Struct("data", + UBInt16("year"), + UBInt8("month"), + UBInt8("day"), + UBInt8("hour"), + UBInt8("minute"), + UBInt8("second"), +) + +#=============================================================================== +# chunks +#=============================================================================== +default_chunk_info = OnDemand( + HexDumpAdapter(Field(None, lambda ctx: ctx.length)) +) + +chunk = Struct("chunk", + UBInt32("length"), + String("type", 4), + Switch("data", lambda ctx: ctx.type, + { + "PLTE" : plte_info, + "IEND" : Pass, + "IDAT" : idat_info, + "tRNS" : trns_info, + "cHRM" : chrm_info, + "gAMA" : gama_info, + "iCCP" : iccp_info, + "sBIT" : sbit_info, + "sRGB" : srgb_info, + "tEXt" : text_info, + "zTXt" : ztxt_info, + "iTXt" : itxt_info, + "bKGD" : bkgd_info, + "hIST" : hist_info, + "pHYs" : phys_info, + "sPLT" : splt_info, + "tIME" : time_info, + }, + default = default_chunk_info, + ), + UBInt32("crc"), +) + +image_header_chunk = Struct("image_header", + UBInt32("length"), + Const(String("type", 4), "IHDR"), + UBInt32("width"), + UBInt32("height"), + UBInt8("bit_depth"), + Enum(UBInt8("color_type"), + greyscale = 0, + truecolor = 2, + indexed = 3, + greywithalpha = 4, + truewithalpha = 6, + _default_ = Pass, + ), + compression_method, + Enum(UBInt8("filter_method"), + # "adaptive filtering with five basic filter types" + adaptive5 = 0, + _default_ = Pass, + ), + Enum(UBInt8("interlace_method"), + none = 0, + adam7 = 1, + _default_ = Pass, + ), + UBInt32("crc"), +) + + +#=============================================================================== +# the complete PNG file +#=============================================================================== +png_file = Struct("png", + Magic(six.b("\x89PNG\r\n\x1a\n")), + image_header_chunk, + Rename("chunks", GreedyRange(chunk)), +) diff --git a/external/construct/formats/graphics/wmf.py b/external/construct/formats/graphics/wmf.py new file mode 100644 index 0000000..55e79dd --- /dev/null +++ b/external/construct/formats/graphics/wmf.py @@ -0,0 +1,129 @@ +""" +Windows Meta File +""" +from construct import * + + +wmf_record = Struct("records", + ULInt32("size"), # size in words, including the size, function and params + Enum(ULInt16("function"), + AbortDoc = 0x0052, + Aldus_Header = 0x0001, + AnimatePalette = 0x0436, + Arc = 0x0817, + BitBlt = 0x0922, + Chord = 0x0830, + CLP_Header16 = 0x0002, + CLP_Header32 = 0x0003, + CreateBitmap = 0x06FE, + CreateBitmapIndirect = 0x02FD, + CreateBrush = 0x00F8, + CreateBrushIndirect = 0x02FC, + CreateFontIndirect = 0x02FB, + CreatePalette = 0x00F7, + CreatePatternBrush = 0x01F9, + CreatePenIndirect = 0x02FA, + CreateRegion = 0x06FF, + DeleteObject = 0x01F0, + DibBitblt = 0x0940, + DibCreatePatternBrush = 0x0142, + DibStretchBlt = 0x0B41, + DrawText = 0x062F, + Ellipse = 0x0418, + EndDoc = 0x005E, + EndPage = 0x0050, + EOF = 0x0000, + Escape = 0x0626, + ExcludeClipRect = 0x0415, + ExtFloodFill = 0x0548, + ExtTextOut = 0x0A32, + FillRegion = 0x0228, + FloodFill = 0x0419, + FrameRegion = 0x0429, + Header = 0x0004, + IntersectClipRect = 0x0416, + InvertRegion = 0x012A, + LineTo = 0x0213, + MoveTo = 0x0214, + OffsetClipRgn = 0x0220, + OffsetViewportOrg = 0x0211, + OffsetWindowOrg = 0x020F, + PaintRegion = 0x012B, + PatBlt = 0x061D, + Pie = 0x081A, + Polygon = 0x0324, + Polyline = 0x0325, + PolyPolygon = 0x0538, + RealizePalette = 0x0035, + Rectangle = 0x041B, + ResetDC = 0x014C, + ResizePalette = 0x0139, + RestoreDC = 0x0127, + RoundRect = 0x061C, + SaveDC = 0x001E, + ScaleViewportExt = 0x0412, + ScaleWindowExt = 0x0410, + SelectClipRegion = 0x012C, + SelectObject = 0x012D, + SelectPalette = 0x0234, + SetBKColor = 0x0201, + SetBKMode = 0x0102, + SetDibToDev = 0x0D33, + SelLayout = 0x0149, + SetMapMode = 0x0103, + SetMapperFlags = 0x0231, + SetPalEntries = 0x0037, + SetPixel = 0x041F, + SetPolyFillMode = 0x0106, + SetReLabs = 0x0105, + SetROP2 = 0x0104, + SetStretchBltMode = 0x0107, + SetTextAlign = 0x012E, + SetTextCharExtra = 0x0108, + SetTextColor = 0x0209, + SetTextJustification = 0x020A, + SetViewportExt = 0x020E, + SetViewportOrg = 0x020D, + SetWindowExt = 0x020C, + SetWindowOrg = 0x020B, + StartDoc = 0x014D, + StartPage = 0x004F, + StretchBlt = 0x0B23, + StretchDIB = 0x0F43, + TextOut = 0x0521, + _default_ = Pass, + ), + Array(lambda ctx: ctx.size - 3, ULInt16("params")), +) + +wmf_placeable_header = Struct("placeable_header", + Const(ULInt32("key"), 0x9AC6CDD7), + ULInt16("handle"), + SLInt16("left"), + SLInt16("top"), + SLInt16("right"), + SLInt16("bottom"), + ULInt16("units_per_inch"), + Padding(4), + ULInt16("checksum") +) + +wmf_file = Struct("wmf_file", + # --- optional placeable header --- + Optional(wmf_placeable_header), + + # --- header --- + Enum(ULInt16("type"), + InMemory = 0, + File = 1, + ), + Const(ULInt16("header_size"), 9), + ULInt16("version"), + ULInt32("size"), # file size is in words + ULInt16("number_of_objects"), + ULInt32("size_of_largest_record"), + ULInt16("number_of_params"), + + # --- records --- + GreedyRange(wmf_record) +) diff --git a/external/construct/lib/__init__.py b/external/construct/lib/__init__.py new file mode 100644 index 0000000..2d533e8 --- /dev/null +++ b/external/construct/lib/__init__.py @@ -0,0 +1,5 @@ +from construct.lib.binary import int_to_bin, bin_to_int, swap_bytes, encode_bin, decode_bin +from construct.lib.bitstream import BitStreamReader, BitStreamWriter +from construct.lib.container import (Container, FlagsContainer, ListContainer, + LazyContainer) +from construct.lib.hex import HexString, hexdump diff --git a/external/construct/lib/__init__.pyc b/external/construct/lib/__init__.pyc Binary files differnew file mode 100644 index 0000000..6eda7eb --- /dev/null +++ b/external/construct/lib/__init__.pyc diff --git a/external/construct/lib/binary.py b/external/construct/lib/binary.py new file mode 100644 index 0000000..c5ef9b3 --- /dev/null +++ b/external/construct/lib/binary.py @@ -0,0 +1,187 @@ +import six +from construct.lib.py3compat import int2byte + + +if six.PY3: + def int_to_bin(number, width = 32): + r""" + Convert an integer into its binary representation in a bytes object. + Width is the amount of bits to generate. If width is larger than the actual + amount of bits required to represent number in binary, sign-extension is + used. If it's smaller, the representation is trimmed to width bits. + Each "bit" is either '\x00' or '\x01'. The MSBit is first. + + Examples: + + >>> int_to_bin(19, 5) + b'\x01\x00\x00\x01\x01' + >>> int_to_bin(19, 8) + b'\x00\x00\x00\x01\x00\x00\x01\x01' + """ + number = int(number) + if number < 0: + number += 1 << width + i = width - 1 + bits = bytearray(width) + while number and i >= 0: + bits[i] = number & 1 + number >>= 1 + i -= 1 + return bytes(bits) + + # heavily optimized for performance + def bin_to_int(bits, signed = False): + r""" + Logical opposite of int_to_bin. Both '0' and '\x00' are considered zero, + and both '1' and '\x01' are considered one. Set sign to True to interpret + the number as a 2-s complement signed integer. + """ + bits = "".join("01"[b & 1] for b in bits) + if signed and bits[0] == "1": + bits = bits[1:] + bias = 1 << len(bits) + else: + bias = 0 + return int(bits, 2) - bias + + _char_to_bin = [0] * 256 + _bin_to_char = {} + for i in range(256): + ch = int2byte(i) + bin = int_to_bin(i, 8) + # Populate with for both keys i and ch, to support Python 2 & 3 + _char_to_bin[i] = bin + _bin_to_char[bin] = ord(ch) + + def encode_bin(data): + """ + Create a binary representation of the given b'' object. Assume 8-bit + ASCII. Example: + + >>> encode_bin('ab') + b"\x00\x01\x01\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x01\x00" + """ + return six.b("").join(_char_to_bin[int(ch)] for ch in data) + + def decode_bin(data): + if len(data) & 7: + raise ValueError("Data length must be a multiple of 8") + i = 0 + j = 0 + l = len(data) // 8 + arr = bytearray(l) + while j < l: + arr[j] = _bin_to_char[data[i:i+8]] + i += 8 + j += 1 + return arr + + def swap_bytes(bits, bytesize=8): + r""" + Bits is a b'' object containing a binary representation. Assuming each + bytesize bits constitute a bytes, perform a endianness byte swap. Example: + + >>> swap_bytes(b'00011011', 2) + b'11100100' + """ + i = 0 + l = len(bits) + output = [six.b("")] * ((l // bytesize) + 1) + j = len(output) - 1 + while i < l: + output[j] = bits[i : i + bytesize] + i += bytesize + j -= 1 + return six.b("").join(output) + +else: + + def int_to_bin(number, width = 32): + r""" + Convert an integer into its binary representation in a bytes object. + Width is the amount of bits to generate. If width is larger than the actual + amount of bits required to represent number in binary, sign-extension is + used. If it's smaller, the representation is trimmed to width bits. + Each "bit" is either '\x00' or '\x01'. The MSBit is first. + + Examples: + + >>> int_to_bin(19, 5) + '\x01\x00\x00\x01\x01' + >>> int_to_bin(19, 8) + '\x00\x00\x00\x01\x00\x00\x01\x01' + """ + if number < 0: + number += 1 << width + i = width - 1 + bits = ["\x00"] * width + while number and i >= 0: + bits[i] = "\x00\x01"[number & 1] + number >>= 1 + i -= 1 + return "".join(bits) + + # heavily optimized for performance + def bin_to_int(bits, signed = False): + r""" + Logical opposite of int_to_bin. Both '0' and '\x00' are considered zero, + and both '1' and '\x01' are considered one. Set sign to True to interpret + the number as a 2-s complement signed integer. + """ + bits = "".join("01"[ord(b) & 1] for b in bits) + if signed and bits[0] == "1": + bits = bits[1:] + bias = 1 << len(bits) + else: + bias = 0 + return int(bits, 2) - bias + + _char_to_bin = [0] * 256 + _bin_to_char = {} + for i in range(256): + ch = int2byte(i) + bin = int_to_bin(i, 8) + # Populate with for both keys i and ch, to support Python 2 & 3 + _char_to_bin[i] = bin + _bin_to_char[bin] = ch + + def encode_bin(data): + """ + Create a binary representation of the given b'' object. Assume 8-bit + ASCII. Example: + + >>> encode_bin('ab') + b"\x00\x01\x01\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x01\x00" + """ + return "".join(_char_to_bin[ord(ch)] for ch in data) + + def decode_bin(data): + if len(data) & 7: + raise ValueError("Data length must be a multiple of 8") + i = 0 + j = 0 + l = len(data) // 8 + chars = [""] * l + while j < l: + chars[j] = _bin_to_char[data[i:i+8]] + i += 8 + j += 1 + return "".join(chars) + + def swap_bytes(bits, bytesize=8): + r""" + Bits is a b'' object containing a binary representation. Assuming each + bytesize bits constitute a bytes, perform a endianness byte swap. Example: + + >>> swap_bytes(b'00011011', 2) + b'11100100' + """ + i = 0 + l = len(bits) + output = [""] * ((l // bytesize) + 1) + j = len(output) - 1 + while i < l: + output[j] = bits[i : i + bytesize] + i += bytesize + j -= 1 + return "".join(output) diff --git a/external/construct/lib/binary.pyc b/external/construct/lib/binary.pyc Binary files differnew file mode 100644 index 0000000..1614005 --- /dev/null +++ b/external/construct/lib/binary.pyc diff --git a/external/construct/lib/bitstream.py b/external/construct/lib/bitstream.py new file mode 100644 index 0000000..3b51f66 --- /dev/null +++ b/external/construct/lib/bitstream.py @@ -0,0 +1,81 @@ +import six +from construct.lib.binary import encode_bin, decode_bin + +try: + bytes +except NameError: + bytes = str + +class BitStreamReader(object): + __slots__ = ["substream", "buffer", "total_size"] + + def __init__(self, substream): + self.substream = substream + self.total_size = 0 + self.buffer = six.b("") + + def close(self): + if self.total_size % 8 != 0: + raise ValueError("total size of read data must be a multiple of 8", + self.total_size) + + def tell(self): + return self.substream.tell() + + def seek(self, pos, whence = 0): + self.buffer = six.b("") + self.total_size = 0 + self.substream.seek(pos, whence) + + def read(self, count): + if count < 0: + raise ValueError("count cannot be negative") + + l = len(self.buffer) + if count == 0: + data = six.b("") + elif count <= l: + data = self.buffer[:count] + self.buffer = self.buffer[count:] + else: + data = self.buffer + count -= l + count_bytes = count // 8 + if count & 7: + count_bytes += 1 + buf = encode_bin(self.substream.read(count_bytes)) + data += buf[:count] + self.buffer = buf[count:] + self.total_size += len(data) + return data + +class BitStreamWriter(object): + __slots__ = ["substream", "buffer", "pos"] + + def __init__(self, substream): + self.substream = substream + self.buffer = [] + self.pos = 0 + + def close(self): + self.flush() + + def flush(self): + raw = decode_bin(six.b("").join(self.buffer)) + self.substream.write(raw) + self.buffer = [] + self.pos = 0 + + def tell(self): + return self.substream.tell() + self.pos // 8 + + def seek(self, pos, whence = 0): + self.flush() + self.substream.seek(pos, whence) + + def write(self, data): + if not data: + return + if not isinstance(data, bytes): + raise TypeError("data must be a string, not %r" % (type(data),)) + self.buffer.append(data) diff --git a/external/construct/lib/bitstream.pyc b/external/construct/lib/bitstream.pyc Binary files differnew file mode 100644 index 0000000..8ea4399 --- /dev/null +++ b/external/construct/lib/bitstream.pyc diff --git a/external/construct/lib/container.py b/external/construct/lib/container.py new file mode 100644 index 0000000..f04d037 --- /dev/null +++ b/external/construct/lib/container.py @@ -0,0 +1,224 @@ +""" +Various containers. +""" + +def recursion_lock(retval, lock_name = "__recursion_lock__"): + def decorator(func): + def wrapper(self, *args, **kw): + if getattr(self, lock_name, False): + return retval + setattr(self, lock_name, True) + try: + return func(self, *args, **kw) + finally: + setattr(self, lock_name, False) + wrapper.__name__ = func.__name__ + return wrapper + return decorator + +class Container(dict): + """ + A generic container of attributes. + + Containers are the common way to express parsed data. + """ + __slots__ = ["__keys_order__"] + + def __init__(self, **kw): + object.__setattr__(self, "__keys_order__", []) + for k, v in kw.items(): + self[k] = v + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + def __setitem__(self, key, val): + if key not in self: + self.__keys_order__.append(key) + dict.__setitem__(self, key, val) + def __delitem__(self, key): + dict.__delitem__(self, key) + self.__keys_order__.remove(key) + + __delattr__ = __delitem__ + __setattr__ = __setitem__ + + def clear(self): + dict.clear(self) + del self.__keys_order__[:] + def pop(self, key, *default): + val = dict.pop(self, key, *default) + self.__keys_order__.remove(key) + return val + def popitem(self): + k, v = dict.popitem(self) + self.__keys_order__.remove(k) + return k, v + + def update(self, seq, **kw): + if hasattr(seq, "keys"): + for k in seq.keys(): + self[k] = seq[k] + else: + for k, v in seq: + self[k] = v + dict.update(self, kw) + + def copy(self): + inst = self.__class__() + inst.update(self.iteritems()) + return inst + + __update__ = update + __copy__ = copy + + def __iter__(self): + return iter(self.__keys_order__) + iterkeys = __iter__ + def itervalues(self): + return (self[k] for k in self.__keys_order__) + def iteritems(self): + return ((k, self[k]) for k in self.__keys_order__) + def keys(self): + return self.__keys_order__ + def values(self): + return list(self.itervalues()) + def items(self): + return list(self.iteritems()) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self)) + + @recursion_lock("<...>") + def __pretty_str__(self, nesting = 1, indentation = " "): + attrs = [] + ind = indentation * nesting + for k, v in self.iteritems(): + if not k.startswith("_"): + text = [ind, k, " = "] + if hasattr(v, "__pretty_str__"): + text.append(v.__pretty_str__(nesting + 1, indentation)) + else: + text.append(repr(v)) + attrs.append("".join(text)) + if not attrs: + return "%s()" % (self.__class__.__name__,) + attrs.insert(0, self.__class__.__name__ + ":") + return "\n".join(attrs) + + __str__ = __pretty_str__ + + +class FlagsContainer(Container): + """ + A container providing pretty-printing for flags. + + Only set flags are displayed. + """ + + @recursion_lock("<...>") + def __pretty_str__(self, nesting = 1, indentation = " "): + attrs = [] + ind = indentation * nesting + for k in self.keys(): + v = self[k] + if not k.startswith("_") and v: + attrs.append(ind + k) + if not attrs: + return "%s()" % (self.__class__.__name__,) + attrs.insert(0, self.__class__.__name__+ ":") + return "\n".join(attrs) + + +class ListContainer(list): + """ + A container for lists. + """ + __slots__ = ["__recursion_lock__"] + + def __str__(self): + return self.__pretty_str__() + + @recursion_lock("[...]") + def __pretty_str__(self, nesting = 1, indentation = " "): + if not self: + return "[]" + ind = indentation * nesting + lines = ["["] + for elem in self: + lines.append("\n") + lines.append(ind) + if hasattr(elem, "__pretty_str__"): + lines.append(elem.__pretty_str__(nesting + 1, indentation)) + else: + lines.append(repr(elem)) + lines.append("\n") + lines.append(indentation * (nesting - 1)) + lines.append("]") + return "".join(lines) + + +class LazyContainer(object): + + __slots__ = ["subcon", "stream", "pos", "context", "_value"] + + def __init__(self, subcon, stream, pos, context): + self.subcon = subcon + self.stream = stream + self.pos = pos + self.context = context + self._value = NotImplemented + + def __eq__(self, other): + try: + return self._value == other._value + except AttributeError: + return False + + def __ne__(self, other): + return not (self == other) + + def __str__(self): + return self.__pretty_str__() + + def __pretty_str__(self, nesting = 1, indentation = " "): + if self._value is NotImplemented: + text = "<unread>" + elif hasattr(self._value, "__pretty_str__"): + text = self._value.__pretty_str__(nesting, indentation) + else: + text = str(self._value) + return "%s: %s" % (self.__class__.__name__, text) + + def read(self): + self.stream.seek(self.pos) + return self.subcon._parse(self.stream, self.context) + + def dispose(self): + self.subcon = None + self.stream = None + self.context = None + self.pos = None + + def _get_value(self): + if self._value is NotImplemented: + self._value = self.read() + return self._value + + value = property(_get_value) + + has_value = property(lambda self: self._value is not NotImplemented) + + + +if __name__ == "__main__": + c = Container(x=5) + c.y = 8 + c.z = 9 + c.w = 10 + c.foo = 5 + + print (c) + + diff --git a/external/construct/lib/container.pyc b/external/construct/lib/container.pyc Binary files differnew file mode 100644 index 0000000..690afbb --- /dev/null +++ b/external/construct/lib/container.pyc diff --git a/external/construct/lib/expr.py b/external/construct/lib/expr.py new file mode 100644 index 0000000..783c947 --- /dev/null +++ b/external/construct/lib/expr.py @@ -0,0 +1,166 @@ +import operator + +if not hasattr(operator, "div"): + operator.div = operator.truediv + + +opnames = { + operator.add : "+", + operator.sub : "-", + operator.mul : "*", + operator.div : "/", + operator.floordiv : "//", + operator.mod : "+", + operator.pow : "**", + operator.xor : "^", + operator.lshift : "<<", + operator.rshift : ">>", + operator.and_ : "and", + operator.or_ : "or", + operator.not_ : "not", + operator.neg : "-", + operator.pos : "+", + operator.contains : "in", + operator.gt : ">", + operator.ge : ">=", + operator.lt : "<", + operator.le : "<=", + operator.eq : "==", + operator.ne : "!=", +} + + +class ExprMixin(object): + __slots__ = () + def __add__(self, other): + return BinExpr(operator.add, self, other) + def __sub__(self, other): + return BinExpr(operator.sub, self, other) + def __mul__(self, other): + return BinExpr(operator.mul, self, other) + def __floordiv__(self, other): + return BinExpr(operator.floordiv, self, other) + def __truediv__(self, other): + return BinExpr(operator.div, self, other) + __div__ = __floordiv__ + def __mod__(self, other): + return BinExpr(operator.mod, self, other) + def __pow__(self, other): + return BinExpr(operator.pow, self, other) + def __xor__(self, other): + return BinExpr(operator.xor, self, other) + def __rshift__(self, other): + return BinExpr(operator.rshift, self, other) + def __lshift__(self, other): + return BinExpr(operator.rshift, self, other) + def __and__(self, other): + return BinExpr(operator.and_, self, other) + def __or__(self, other): + return BinExpr(operator.or_, self, other) + + def __radd__(self, other): + return BinExpr(operator.add, other, self) + def __rsub__(self, other): + return BinExpr(operator.sub, other, self) + def __rmul__(self, other): + return BinExpr(operator.mul, other, self) + def __rfloordiv__(self, other): + return BinExpr(operator.floordiv, other, self) + def __rtruediv__(self, other): + return BinExpr(operator.div, other, self) + __rdiv__ = __rfloordiv__ + def __rmod__(self, other): + return BinExpr(operator.mod, other, self) + def __rpow__(self, other): + return BinExpr(operator.pow, other, self) + def __rxor__(self, other): + return BinExpr(operator.xor, other, self) + def __rrshift__(self, other): + return BinExpr(operator.rshift, other, self) + def __rlshift__(self, other): + return BinExpr(operator.rshift, other, self) + def __rand__(self, other): + return BinExpr(operator.and_, other, self) + def __ror__(self, other): + return BinExpr(operator.or_, other, self) + + def __neg__(self): + return UniExpr(operator.neg, self) + def __pos__(self): + return UniExpr(operator.pos, self) + def __invert__(self): + return UniExpr(operator.not_, self) + __inv__ = __invert__ + + def __contains__(self, other): + return BinExpr(operator.contains, self, other) + def __gt__(self, other): + return BinExpr(operator.gt, self, other) + def __ge__(self, other): + return BinExpr(operator.ge, self, other) + def __lt__(self, other): + return BinExpr(operator.lt, self, other) + def __le__(self, other): + return BinExpr(operator.le, self, other) + def __eq__(self, other): + return BinExpr(operator.eq, self, other) + def __ne__(self, other): + return BinExpr(operator.ne, self, other) + + +class UniExpr(ExprMixin): + __slots__ = ["op", "operand"] + def __init__(self, op, operand): + self.op = op + self.operand = operand + def __repr__(self): + return "%s %r" % (opnames[self.op], self.operand) + def __call__(self, context): + operand = self.operand(context) if callable(self.operand) else self.operand + return self.op(operand) + + +class BinExpr(ExprMixin): + __slots__ = ["op", "lhs", "rhs"] + def __init__(self, op, lhs, rhs): + self.op = op + self.lhs = lhs + self.rhs = rhs + def __repr__(self): + return "(%r %s %r)" % (self.lhs, opnames[self.op], self.rhs) + def __call__(self, context): + lhs = self.lhs(context) if callable(self.lhs) else self.lhs + rhs = self.rhs(context) if callable(self.rhs) else self.rhs + return self.op(lhs, rhs) + + +class Path(ExprMixin): + __slots__ = ["__name", "__parent"] + def __init__(self, name, parent = None): + self.__name = name + self.__parent = parent + def __repr__(self): + if self.__parent is None: + return self.__name + return "%r.%s" % (self.__parent, self.__name) + def __call__(self, context): + if self.__parent is None: + return context + context2 = self.__parent(context) + return context2[self.__name] + def __getattr__(self, name): + return Path(name, self) + + +# let the magic begin! +this = Path("this") + + +if __name__ == "__main__": + x = ~((this.foo * 2 + 3 << 2) % 11) + print (x) + print (x({"foo" : 7})) + + + + diff --git a/external/construct/lib/expr.pyc b/external/construct/lib/expr.pyc Binary files differnew file mode 100644 index 0000000..14e1ca2 --- /dev/null +++ b/external/construct/lib/expr.pyc diff --git a/external/construct/lib/hex.py b/external/construct/lib/hex.py new file mode 100644 index 0000000..62fd3c7 --- /dev/null +++ b/external/construct/lib/hex.py @@ -0,0 +1,46 @@ +from construct.lib.py3compat import byte2int, int2byte, bytes2str + + +# Map an integer in the inclusive range 0-255 to its string byte representation +_printable = dict((i, ".") for i in range(256)) +_printable.update((i, bytes2str(int2byte(i))) for i in range(32, 128)) + + +def hexdump(data, linesize): + """ + data is a bytes object. The returned result is a string. + """ + prettylines = [] + if len(data) < 65536: + fmt = "%%04X %%-%ds %%s" + else: + fmt = "%%08X %%-%ds %%s" + fmt = fmt % (3 * linesize - 1,) + for i in range(0, len(data), linesize): + line = data[i : i + linesize] + hextext = " ".join('%02x' % byte2int(b) for b in line) + rawtext = "".join(_printable[byte2int(b)] for b in line) + prettylines.append(fmt % (i, str(hextext), str(rawtext))) + return prettylines + + +try: + basecls = bytes +except NameError: + basecls = str + +class HexString(basecls): + """ + Represents bytes that will be hex-dumped to a string when its string + representation is requested. + """ + def __init__(self, data, linesize = 16): + self.linesize = linesize + def __new__(cls, data, *args, **kwargs): + return basecls.__new__(cls, data) + def __str__(self): + if not self: + return "''" + return "\n" + "\n".join(hexdump(self, self.linesize)) + + diff --git a/external/construct/lib/hex.pyc b/external/construct/lib/hex.pyc Binary files differnew file mode 100644 index 0000000..bc766fa --- /dev/null +++ b/external/construct/lib/hex.pyc diff --git a/external/construct/lib/py3compat.py b/external/construct/lib/py3compat.py new file mode 100644 index 0000000..4a52c29 --- /dev/null +++ b/external/construct/lib/py3compat.py @@ -0,0 +1,70 @@ +#------------------------------------------------------------------------------- +# py3compat.py +# +# Some Python2&3 compatibility code +#------------------------------------------------------------------------------- +import sys +PY3 = sys.version_info[0] == 3 + + +if PY3: + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + + def bchr(i): + """ When iterating over b'...' in Python 2 you get single b'_' chars + and in Python 3 you get integers. Call bchr to always turn this + to single b'_' chars. + """ + return bytes((i,)) + + def u(s): + return s + + def int2byte(i): + return bytes((i,)) + + def byte2int(b): + return b + + def str2bytes(s): + return s.encode("latin-1") + + def str2unicode(s): + return s + + def bytes2str(b): + return b.decode('latin-1') + + def decodebytes(b, encoding): + return bytes(b, encoding) + + advance_iterator = next + +else: + import cStringIO + StringIO = BytesIO = cStringIO.StringIO + + int2byte = chr + byte2int = ord + bchr = lambda i: i + + def u(s): + return unicode(s, "unicode_escape") + + def str2bytes(s): + return s + + def str2unicode(s): + return unicode(s, "unicode_escape") + + def bytes2str(b): + return b + + def decodebytes(b, encoding): + return b.decode(encoding) + + def advance_iterator(it): + return it.next() + diff --git a/external/construct/lib/py3compat.pyc b/external/construct/lib/py3compat.pyc Binary files differnew file mode 100644 index 0000000..4f06b7b --- /dev/null +++ b/external/construct/lib/py3compat.pyc diff --git a/external/construct/macros.py b/external/construct/macros.py new file mode 100644 index 0000000..bbcbd33 --- /dev/null +++ b/external/construct/macros.py @@ -0,0 +1,676 @@ +import six +from construct.lib.py3compat import int2byte +from construct.lib import (BitStreamReader, BitStreamWriter, encode_bin, decode_bin) +from construct.core import (Struct, MetaField, StaticField, FormatField, + OnDemand, Pointer, Switch, Value, RepeatUntil, MetaArray, Sequence, Range, + Select, Pass, SizeofError, Buffered, Restream, Reconfig) +from construct.adapters import (BitIntegerAdapter, PaddingAdapter, + ConstAdapter, CStringAdapter, LengthValueAdapter, IndexingAdapter, + PaddedStringAdapter, FlagsAdapter, StringAdapter, MappingAdapter) +try: + from sys import maxsize +except ImportError: + from sys import maxint as maxsize + + +#=============================================================================== +# fields +#=============================================================================== +def Field(name, length): + """ + A field consisting of a specified number of bytes. + + :param name: the name of the field + :param length: the length of the field. the length can be either an integer + (StaticField), or a function that takes the context as an argument and + returns the length (MetaField) + """ + if callable(length): + return MetaField(name, length) + else: + return StaticField(name, length) + +def BitField(name, length, swapped = False, signed = False, bytesize = 8): + r""" + BitFields, as the name suggests, are fields that operate on raw, unaligned + bits, and therefore must be enclosed in a BitStruct. Using them is very + similar to all normal fields: they take a name and a length (in bits). + + :param name: name of the field + :param length: number of bits in the field, or a function that takes + the context as its argument and returns the length + :param swapped: whether the value is byte-swapped + :param signed: whether the value is signed + :param bytesize: number of bits per byte, for byte-swapping + + Example:: + + >>> foo = BitStruct("foo", + ... BitField("a", 3), + ... Flag("b"), + ... Padding(3), + ... Nibble("c"), + ... BitField("d", 5), + ... ) + >>> foo.parse("\xe1\x1f") + Container(a = 7, b = False, c = 8, d = 31) + >>> foo = BitStruct("foo", + ... BitField("a", 3), + ... Flag("b"), + ... Padding(3), + ... Nibble("c"), + ... Struct("bar", + ... Nibble("d"), + ... Bit("e"), + ... ) + ... ) + >>> foo.parse("\xe1\x1f") + Container(a = 7, b = False, bar = Container(d = 15, e = 1), c = 8) + """ + + return BitIntegerAdapter(Field(name, length), + length, + swapped=swapped, + signed=signed, + bytesize=bytesize + ) + +def Padding(length, pattern = six.b("\x00"), strict = False): + r"""A padding field (value is discarded) + + :param length: the length of the field. the length can be either an integer, + or a function that takes the context as an argument and returns the length + :param pattern: the padding pattern (character) to use. default is "\x00" + :param strict: whether or not to raise an exception is the actual padding + pattern mismatches the desired pattern. default is False. + """ + return PaddingAdapter(Field(None, length), + pattern = pattern, + strict = strict, + ) + +def Flag(name, truth = 1, falsehood = 0, default = False): + """ + A flag. + + Flags are usually used to signify a Boolean value, and this construct + maps values onto the ``bool`` type. + + .. note:: This construct works with both bit and byte contexts. + + .. warning:: Flags default to False, not True. This is different from the + C and Python way of thinking about truth, and may be subject to change + in the future. + + :param name: field name + :param truth: value of truth (default 1) + :param falsehood: value of falsehood (default 0) + :param default: default value (default False) + """ + + return SymmetricMapping(Field(name, 1), + {True : int2byte(truth), False : int2byte(falsehood)}, + default = default, + ) + +#=============================================================================== +# field shortcuts +#=============================================================================== +def Bit(name): + """A 1-bit BitField; must be enclosed in a BitStruct""" + return BitField(name, 1) +def Nibble(name): + """A 4-bit BitField; must be enclosed in a BitStruct""" + return BitField(name, 4) +def Octet(name): + """An 8-bit BitField; must be enclosed in a BitStruct""" + return BitField(name, 8) + +def UBInt8(name): + """Unsigned, big endian 8-bit integer""" + return FormatField(name, ">", "B") +def UBInt16(name): + """Unsigned, big endian 16-bit integer""" + return FormatField(name, ">", "H") +def UBInt32(name): + """Unsigned, big endian 32-bit integer""" + return FormatField(name, ">", "L") +def UBInt64(name): + """Unsigned, big endian 64-bit integer""" + return FormatField(name, ">", "Q") + +def SBInt8(name): + """Signed, big endian 8-bit integer""" + return FormatField(name, ">", "b") +def SBInt16(name): + """Signed, big endian 16-bit integer""" + return FormatField(name, ">", "h") +def SBInt32(name): + """Signed, big endian 32-bit integer""" + return FormatField(name, ">", "l") +def SBInt64(name): + """Signed, big endian 64-bit integer""" + return FormatField(name, ">", "q") + +def ULInt8(name): + """Unsigned, little endian 8-bit integer""" + return FormatField(name, "<", "B") +def ULInt16(name): + """Unsigned, little endian 16-bit integer""" + return FormatField(name, "<", "H") +def ULInt32(name): + """Unsigned, little endian 32-bit integer""" + return FormatField(name, "<", "L") +def ULInt64(name): + """Unsigned, little endian 64-bit integer""" + return FormatField(name, "<", "Q") + +def SLInt8(name): + """Signed, little endian 8-bit integer""" + return FormatField(name, "<", "b") +def SLInt16(name): + """Signed, little endian 16-bit integer""" + return FormatField(name, "<", "h") +def SLInt32(name): + """Signed, little endian 32-bit integer""" + return FormatField(name, "<", "l") +def SLInt64(name): + """Signed, little endian 64-bit integer""" + return FormatField(name, "<", "q") + +def UNInt8(name): + """Unsigned, native endianity 8-bit integer""" + return FormatField(name, "=", "B") +def UNInt16(name): + """Unsigned, native endianity 16-bit integer""" + return FormatField(name, "=", "H") +def UNInt32(name): + """Unsigned, native endianity 32-bit integer""" + return FormatField(name, "=", "L") +def UNInt64(name): + """Unsigned, native endianity 64-bit integer""" + return FormatField(name, "=", "Q") + +def SNInt8(name): + """Signed, native endianity 8-bit integer""" + return FormatField(name, "=", "b") +def SNInt16(name): + """Signed, native endianity 16-bit integer""" + return FormatField(name, "=", "h") +def SNInt32(name): + """Signed, native endianity 32-bit integer""" + return FormatField(name, "=", "l") +def SNInt64(name): + """Signed, native endianity 64-bit integer""" + return FormatField(name, "=", "q") + +def BFloat32(name): + """Big endian, 32-bit IEEE floating point number""" + return FormatField(name, ">", "f") +def LFloat32(name): + """Little endian, 32-bit IEEE floating point number""" + return FormatField(name, "<", "f") +def NFloat32(name): + """Native endianity, 32-bit IEEE floating point number""" + return FormatField(name, "=", "f") + +def BFloat64(name): + """Big endian, 64-bit IEEE floating point number""" + return FormatField(name, ">", "d") +def LFloat64(name): + """Little endian, 64-bit IEEE floating point number""" + return FormatField(name, "<", "d") +def NFloat64(name): + """Native endianity, 64-bit IEEE floating point number""" + return FormatField(name, "=", "d") + + +#=============================================================================== +# arrays +#=============================================================================== +def Array(count, subcon): + r""" + Repeats the given unit a fixed number of times. + + :param count: number of times to repeat + :param subcon: construct to repeat + + Example:: + + >>> c = Array(4, UBInt8("foo")) + >>> c.parse("\x01\x02\x03\x04") + [1, 2, 3, 4] + >>> c.parse("\x01\x02\x03\x04\x05\x06") + [1, 2, 3, 4] + >>> c.build([5,6,7,8]) + '\x05\x06\x07\x08' + >>> c.build([5,6,7,8,9]) + Traceback (most recent call last): + ... + construct.core.RangeError: expected 4..4, found 5 + """ + + if callable(count): + con = MetaArray(count, subcon) + else: + con = MetaArray(lambda ctx: count, subcon) + con._clear_flag(con.FLAG_DYNAMIC) + return con + +def PrefixedArray(subcon, length_field = UBInt8("length")): + """An array prefixed by a length field. + + :param subcon: the subcon to be repeated + :param length_field: a construct returning an integer + """ + def _length(ctx): + if issubclass(ctx.__class__, (list, tuple)): + return len(ctx) + return ctx[length_field.name] + + return LengthValueAdapter( + Sequence(subcon.name, + length_field, + Array(_length, subcon), + nested = False + ) + ) + +def OpenRange(mincount, subcon): + return Range(mincount, maxsize, subcon) + +def GreedyRange(subcon): + r""" + Repeats the given unit one or more times. + + :param subcon: construct to repeat + + Example:: + + >>> from construct import GreedyRange, UBInt8 + >>> c = GreedyRange(UBInt8("foo")) + >>> c.parse("\x01") + [1] + >>> c.parse("\x01\x02\x03") + [1, 2, 3] + >>> c.parse("\x01\x02\x03\x04\x05\x06") + [1, 2, 3, 4, 5, 6] + >>> c.parse("") + Traceback (most recent call last): + ... + construct.core.RangeError: expected 1..2147483647, found 0 + >>> c.build([1,2]) + '\x01\x02' + >>> c.build([]) + Traceback (most recent call last): + ... + construct.core.RangeError: expected 1..2147483647, found 0 + """ + return OpenRange(1, subcon) + +def OptionalGreedyRange(subcon): + r""" + Repeats the given unit zero or more times. This repeater can't + fail, as it accepts lists of any length. + + :param subcon: construct to repeat + + Example:: + + >>> from construct import OptionalGreedyRange, UBInt8 + >>> c = OptionalGreedyRange(UBInt8("foo")) + >>> c.parse("") + [] + >>> c.parse("\x01\x02") + [1, 2] + >>> c.build([]) + '' + >>> c.build([1,2]) + '\x01\x02' + """ + return OpenRange(0, subcon) + + +#=============================================================================== +# subconstructs +#=============================================================================== +def Optional(subcon): + """An optional construct. if parsing fails, returns None. + + :param subcon: the subcon to optionally parse or build + """ + return Select(subcon.name, subcon, Pass) + +def Bitwise(subcon): + """Converts the stream to bits, and passes the bitstream to subcon + + :param subcon: a bitwise construct (usually BitField) + """ + # subcons larger than MAX_BUFFER will be wrapped by Restream instead + # of Buffered. implementation details, don't stick your nose in :) + MAX_BUFFER = 1024 * 8 + def resizer(length): + if length & 7: + raise SizeofError("size must be a multiple of 8", length) + return length >> 3 + if not subcon._is_flag(subcon.FLAG_DYNAMIC) and subcon.sizeof() < MAX_BUFFER: + con = Buffered(subcon, + encoder = decode_bin, + decoder = encode_bin, + resizer = resizer + ) + else: + con = Restream(subcon, + stream_reader = BitStreamReader, + stream_writer = BitStreamWriter, + resizer = resizer) + return con + +def Aligned(subcon, modulus = 4, pattern = six.b("\x00")): + r"""Aligns subcon to modulus boundary using padding pattern + + :param subcon: the subcon to align + :param modulus: the modulus boundary (default is 4) + :param pattern: the padding pattern (default is \x00) + """ + if modulus < 2: + raise ValueError("modulus must be >= 2", modulus) + def padlength(ctx): + return (modulus - (subcon._sizeof(ctx) % modulus)) % modulus + return SeqOfOne(subcon.name, + subcon, + # ?????? + # ?????? + # ?????? + # ?????? + Padding(padlength, pattern = pattern), + nested = False, + ) + +def SeqOfOne(name, *args, **kw): + r"""A sequence of one element. only the first element is meaningful, the + rest are discarded + + :param name: the name of the sequence + :param \*args: subconstructs + :param \*\*kw: any keyword arguments to Sequence + """ + return IndexingAdapter(Sequence(name, *args, **kw), index = 0) + +def Embedded(subcon): + """Embeds a struct into the enclosing struct. + + :param subcon: the struct to embed + """ + return Reconfig(subcon.name, subcon, subcon.FLAG_EMBED) + +def Rename(newname, subcon): + """Renames an existing construct + + :param newname: the new name + :param subcon: the subcon to rename + """ + return Reconfig(newname, subcon) + +def Alias(newname, oldname): + """Creates an alias for an existing element in a struct + + :param newname: the new name + :param oldname: the name of an existing element + """ + return Value(newname, lambda ctx: ctx[oldname]) + + +#=============================================================================== +# mapping +#=============================================================================== +def SymmetricMapping(subcon, mapping, default = NotImplemented): + """Defines a symmetrical mapping: a->b, b->a. + + :param subcon: the subcon to map + :param mapping: the encoding mapping (a dict); the decoding mapping is + achieved by reversing this mapping + :param default: the default value to use when no mapping is found. if no + default value is given, and exception is raised. setting to Pass would + return the value "as is" (unmapped) + """ + reversed_mapping = dict((v, k) for k, v in mapping.items()) + return MappingAdapter(subcon, + encoding = mapping, + decoding = reversed_mapping, + encdefault = default, + decdefault = default, + ) + +def Enum(subcon, **kw): + r"""A set of named values mapping. + + :param subcon: the subcon to map + :param \*\*kw: keyword arguments which serve as the encoding mapping + :param _default_: an optional, keyword-only argument that specifies the + default value to use when the mapping is undefined. if not given, + and exception is raised when the mapping is undefined. use `Pass` to + pass the unmapped value as-is + """ + return SymmetricMapping(subcon, kw, kw.pop("_default_", NotImplemented)) + +def FlagsEnum(subcon, **kw): + r"""A set of flag values mapping. + + :param subcon: the subcon to map + :param \*\*kw: keyword arguments which serve as the encoding mapping + """ + return FlagsAdapter(subcon, kw) + + +#=============================================================================== +# structs +#=============================================================================== +def AlignedStruct(name, *subcons, **kw): + r"""A struct of aligned fields + + :param name: the name of the struct + :param \*subcons: the subcons that make up this structure + :param \*\*kw: keyword arguments to pass to Aligned: 'modulus' and 'pattern' + """ + return Struct(name, *(Aligned(sc, **kw) for sc in subcons)) + +def BitStruct(name, *subcons): + r"""A struct of bitwise fields + + :param name: the name of the struct + :param \*subcons: the subcons that make up this structure + """ + return Bitwise(Struct(name, *subcons)) + +def EmbeddedBitStruct(*subcons): + r"""An embedded BitStruct. no name is necessary. + + :param \*subcons: the subcons that make up this structure + """ + return Bitwise(Embedded(Struct(None, *subcons))) + +#=============================================================================== +# strings +#=============================================================================== +def String(name, length, encoding=None, padchar=None, paddir="right", + trimdir="right"): + r""" + A configurable, fixed-length string field. + + The padding character must be specified for padding and trimming to work. + + :param name: name + :param length: length, in bytes + :param encoding: encoding (e.g. "utf8") or None for no encoding + :param padchar: optional character to pad out strings + :param paddir: direction to pad out strings; one of "right", "left", or "both" + :param str trim: direction to trim strings; one of "right", "left" + + Example:: + + >>> from construct import String + >>> String("foo", 5).parse("hello") + 'hello' + >>> + >>> String("foo", 12, encoding = "utf8").parse("hello joh\xd4\x83n") + u'hello joh\u0503n' + >>> + >>> foo = String("foo", 10, padchar = "X", paddir = "right") + >>> foo.parse("helloXXXXX") + 'hello' + >>> foo.build("hello") + 'helloXXXXX' + """ + con = StringAdapter(Field(name, length), encoding=encoding) + if padchar is not None: + con = PaddedStringAdapter(con, padchar=padchar, paddir=paddir, + trimdir=trimdir) + return con + +def PascalString(name, length_field=UBInt8("length"), encoding=None): + r""" + A length-prefixed string. + + ``PascalString`` is named after the string types of Pascal, which are + length-prefixed. Lisp strings also follow this convention. + + The length field will appear in the same ``Container`` as the + ``PascalString``, with the given name. + + :param name: name + :param length_field: a field which will store the length of the string + :param encoding: encoding (e.g. "utf8") or None for no encoding + + Example:: + + >>> foo = PascalString("foo") + >>> foo.parse("\x05hello") + 'hello' + >>> foo.build("hello world") + '\x0bhello world' + >>> + >>> foo = PascalString("foo", length_field = UBInt16("length")) + >>> foo.parse("\x00\x05hello") + 'hello' + >>> foo.build("hello") + '\x00\x05hello' + """ + + return StringAdapter( + LengthValueAdapter( + Sequence(name, + length_field, + Field("data", lambda ctx: ctx[length_field.name]), + ) + ), + encoding=encoding, + ) + +def CString(name, terminators=six.b("\x00"), encoding=None, + char_field=Field(None, 1)): + r""" + A string ending in a terminator. + + ``CString`` is similar to the strings of C, C++, and other related + programming languages. + + By default, the terminator is the NULL byte (b``0x00``). + + :param name: name + :param terminators: sequence of valid terminators, in order of preference + :param encoding: encoding (e.g. "utf8") or None for no encoding + :param char_field: construct representing a single character + + Example:: + + >>> foo = CString("foo") + >>> foo.parse(b"hello\x00") + b'hello' + >>> foo.build(b"hello") + b'hello\x00' + >>> foo = CString("foo", terminators = b"XYZ") + >>> foo.parse(b"helloX") + b'hello' + >>> foo.parse(b"helloY") + b'hello' + >>> foo.parse(b"helloZ") + b'hello' + >>> foo.build(b"hello") + b'helloX' + """ + + return Rename(name, + CStringAdapter( + RepeatUntil(lambda obj, ctx: obj in terminators, char_field), + terminators=terminators, + encoding=encoding, + ) + ) + + +#=============================================================================== +# conditional +#=============================================================================== +def IfThenElse(name, predicate, then_subcon, else_subcon): + """An if-then-else conditional construct: if the predicate indicates True, + `then_subcon` will be used; otherwise `else_subcon` + + :param name: the name of the construct + :param predicate: a function taking the context as an argument and returning True or False + :param then_subcon: the subcon that will be used if the predicate returns True + :param else_subcon: the subcon that will be used if the predicate returns False + """ + return Switch(name, lambda ctx: bool(predicate(ctx)), + { + True : then_subcon, + False : else_subcon, + } + ) + +def If(predicate, subcon, elsevalue = None): + """An if-then conditional construct: if the predicate indicates True, + subcon will be used; otherwise, `elsevalue` will be returned instead. + + :param predicate: a function taking the context as an argument and returning True or False + :param subcon: the subcon that will be used if the predicate returns True + :param elsevalue: the value that will be used should the predicate return False. + by default this value is None. + """ + return IfThenElse(subcon.name, + predicate, + subcon, + Value("elsevalue", lambda ctx: elsevalue) + ) + + +#=============================================================================== +# misc +#=============================================================================== +def OnDemandPointer(offsetfunc, subcon, force_build = True): + """An on-demand pointer. + + :param offsetfunc: a function taking the context as an argument and returning + the absolute stream position + :param subcon: the subcon that will be parsed from the `offsetfunc()` stream position on demand + :param force_build: see OnDemand. by default True. + """ + return OnDemand(Pointer(offsetfunc, subcon), + advance_stream = False, + force_build = force_build + ) + +def Magic(data): + """A 'magic number' construct. it is used for file signatures, etc., to validate + that the given pattern exists. + + Example:: + + elf_header = Struct("elf_header", + Magic("\x7fELF"), + # ... + ) + """ + return ConstAdapter(Field(None, len(data)), data) + + diff --git a/external/construct/macros.pyc b/external/construct/macros.pyc Binary files differnew file mode 100644 index 0000000..c197a3f --- /dev/null +++ b/external/construct/macros.pyc diff --git a/external/construct/protocols/__init__.py b/external/construct/protocols/__init__.py new file mode 100644 index 0000000..0ec215e --- /dev/null +++ b/external/construct/protocols/__init__.py @@ -0,0 +1,4 @@ +""" +protocols - a collection of network protocols +unlike the formats package, protocols convey information between two sides +""" diff --git a/external/construct/protocols/__init__.pyc b/external/construct/protocols/__init__.pyc Binary files differnew file mode 100644 index 0000000..9048b27 --- /dev/null +++ b/external/construct/protocols/__init__.pyc diff --git a/external/construct/protocols/application/__init__.py b/external/construct/protocols/application/__init__.py new file mode 100644 index 0000000..7ea61f7 --- /dev/null +++ b/external/construct/protocols/application/__init__.py @@ -0,0 +1,4 @@ +""" +application layer (various) protocols +""" + diff --git a/external/construct/protocols/application/dns.py b/external/construct/protocols/application/dns.py new file mode 100644 index 0000000..8a586ce --- /dev/null +++ b/external/construct/protocols/application/dns.py @@ -0,0 +1,147 @@ +""" +Domain Name System (TCP/IP protocol stack) +""" +from construct import * +from construct.protocols.layer3.ipv4 import IpAddressAdapter +from binascii import unhexlify +import six + + +class DnsStringAdapter(Adapter): + def _encode(self, obj, context): + parts = obj.split(".") + parts.append("") + return parts + def _decode(self, obj, context): + return ".".join(obj[:-1]) + +dns_record_class = Enum(UBInt16("class"), + RESERVED = 0, + INTERNET = 1, + CHAOS = 3, + HESIOD = 4, + NONE = 254, + ANY = 255, +) + +dns_record_type = Enum(UBInt16("type"), + IPv4 = 1, + AUTHORITIVE_NAME_SERVER = 2, + CANONICAL_NAME = 5, + NULL = 10, + MAIL_EXCHANGE = 15, + TEXT = 16, + X25 = 19, + ISDN = 20, + IPv6 = 28, + UNSPECIFIED = 103, + ALL = 255, +) + +query_record = Struct("query_record", + DnsStringAdapter( + RepeatUntil(lambda obj, ctx: obj == "", + PascalString("name") + ) + ), + dns_record_type, + dns_record_class, +) + +rdata = Field("rdata", lambda ctx: ctx.rdata_length) + +resource_record = Struct("resource_record", + CString("name", terminators = six.b("\xc0\x00")), + Padding(1), + dns_record_type, + dns_record_class, + UBInt32("ttl"), + UBInt16("rdata_length"), + IfThenElse("data", lambda ctx: ctx.type == "IPv4", + IpAddressAdapter(rdata), + rdata + ) +) + +dns = Struct("dns", + UBInt16("id"), + BitStruct("flags", + Enum(Bit("type"), + QUERY = 0, + RESPONSE = 1, + ), + Enum(Nibble("opcode"), + STANDARD_QUERY = 0, + INVERSE_QUERY = 1, + SERVER_STATUS_REQUEST = 2, + NOTIFY = 4, + UPDATE = 5, + ), + Flag("authoritive_answer"), + Flag("truncation"), + Flag("recurssion_desired"), + Flag("recursion_available"), + Padding(1), + Flag("authenticated_data"), + Flag("checking_disabled"), + Enum(Nibble("response_code"), + SUCCESS = 0, + FORMAT_ERROR = 1, + SERVER_FAILURE = 2, + NAME_DOES_NOT_EXIST = 3, + NOT_IMPLEMENTED = 4, + REFUSED = 5, + NAME_SHOULD_NOT_EXIST = 6, + RR_SHOULD_NOT_EXIST = 7, + RR_SHOULD_EXIST = 8, + NOT_AUTHORITIVE = 9, + NOT_ZONE = 10, + ), + ), + UBInt16("question_count"), + UBInt16("answer_count"), + UBInt16("authority_count"), + UBInt16("additional_count"), + Array(lambda ctx: ctx.question_count, + Rename("questions", query_record), + ), + Rename("answers", + Array(lambda ctx: ctx.answer_count, resource_record) + ), + Rename("authorities", + Array(lambda ctx: ctx.authority_count, resource_record) + ), + Array(lambda ctx: ctx.additional_count, + Rename("additionals", resource_record), + ), +) + + +if __name__ == "__main__": + cap1 = unhexlify(six.b("2624010000010000000000000377777706676f6f676c6503636f6d0000010001")) + + cap2 = unhexlify(six.b( + "2624818000010005000600060377777706676f6f676c6503636f6d0000010001c00c00" + "05000100089065000803777777016cc010c02c0001000100000004000440e9b768c02c" + "0001000100000004000440e9b793c02c0001000100000004000440e9b763c02c000100" + "0100000004000440e9b767c030000200010000a88600040163c030c030000200010000" + "a88600040164c030c030000200010000a88600040165c030c030000200010000a88600" + "040167c030c030000200010000a88600040161c030c030000200010000a88600040162" + "c030c0c00001000100011d0c0004d8ef3509c0d0000100010000ca7c000440e9b309c0" + "80000100010000c4c5000440e9a109c0900001000100004391000440e9b709c0a00001" + "00010000ca7c000442660b09c0b00001000100000266000440e9a709" + )) + + obj = dns.parse(cap1) + print (obj) + print (repr(dns.build(obj))) + + print ("-" * 80) + + obj = dns.parse(cap2) + print (obj) + print (repr(dns.build(obj))) + + + + diff --git a/external/construct/protocols/ipstack.py b/external/construct/protocols/ipstack.py new file mode 100644 index 0000000..7c82fb6 --- /dev/null +++ b/external/construct/protocols/ipstack.py @@ -0,0 +1,137 @@ +""" +TCP/IP Protocol Stack +Note: before parsing the application layer over a TCP stream, you must +first combine all the TCP frames into a stream. See utils.tcpip for +some solutions +""" +from construct import Struct, Rename, HexDumpAdapter, Field, Switch, Pass +from construct.protocols.layer2.ethernet import ethernet_header +from construct.protocols.layer3.ipv4 import ipv4_header +from construct.protocols.layer3.ipv6 import ipv6_header +from construct.protocols.layer4.tcp import tcp_header +from construct.protocols.layer4.udp import udp_header +from binascii import unhexlify +import six + + +layer4_tcp = Struct("layer4_tcp", + Rename("header", tcp_header), + HexDumpAdapter( + Field("next", lambda ctx: + ctx["_"]["header"].payload_length - ctx["header"].header_length + ) + ), +) + +layer4_udp = Struct("layer4_udp", + Rename("header", udp_header), + HexDumpAdapter( + Field("next", lambda ctx: ctx["header"].payload_length) + ), +) + +layer3_payload = Switch("next", lambda ctx: ctx["header"].protocol, + { + "TCP" : layer4_tcp, + "UDP" : layer4_udp, + }, + default = Pass +) + +layer3_ipv4 = Struct("layer3_ipv4", + Rename("header", ipv4_header), + layer3_payload, +) + +layer3_ipv6 = Struct("layer3_ipv6", + Rename("header", ipv6_header), + layer3_payload, +) + +layer2_ethernet = Struct("layer2_ethernet", + Rename("header", ethernet_header), + Switch("next", lambda ctx: ctx["header"].type, + { + "IPv4" : layer3_ipv4, + "IPv6" : layer3_ipv6, + }, + default = Pass, + ) +) + +ip_stack = Rename("ip_stack", layer2_ethernet) + + +if __name__ == "__main__": + cap1 = unhexlify(six.b( + "0011508c283c001150886b570800450001e971474000800684e4c0a80202525eedda11" + "2a0050d98ec61d54fe977d501844705dcc0000474554202f20485454502f312e310d0a" + "486f73743a207777772e707974686f6e2e6f72670d0a557365722d4167656e743a204d" + "6f7a696c6c612f352e30202857696e646f77733b20553b2057696e646f7773204e5420" + "352e313b20656e2d55533b2072763a312e382e302e3129204765636b6f2f3230303630" + "3131312046697265666f782f312e352e302e310d0a4163636570743a20746578742f78" + "6d6c2c6170706c69636174696f6e2f786d6c2c6170706c69636174696f6e2f7868746d" + "6c2b786d6c2c746578742f68746d6c3b713d302e392c746578742f706c61696e3b713d" + "302e382c696d6167652f706e672c2a2f2a3b713d302e350d0a4163636570742d4c616e" + "67756167653a20656e2d75732c656e3b713d302e350d0a4163636570742d456e636f64" + "696e673a20677a69702c6465666c6174650d0a4163636570742d436861727365743a20" + "49534f2d383835392d312c7574662d383b713d302e372c2a3b713d302e370d0a4b6565" + "702d416c6976653a203330300d0a436f6e6e656374696f6e3a206b6565702d616c6976" + "650d0a507261676d613a206e6f2d63616368650d0a43616368652d436f6e74726f6c3a" + "206e6f2d63616368650d0a0d0a" + )) + + cap2 = unhexlify(six.b( + "0002e3426009001150f2c280080045900598fd22000036063291d149baeec0a8023c00" + "500cc33b8aa7dcc4e588065010ffffcecd0000485454502f312e3120323030204f4b0d" + "0a446174653a204672692c2031352044656320323030362032313a32363a323520474d" + "540d0a5033503a20706f6c6963797265663d22687474703a2f2f7033702e7961686f6f" + "2e636f6d2f7733632f7033702e786d6c222c2043503d2243414f2044535020434f5220" + "4355522041444d20444556205441492050534120505344204956416920495644692043" + "4f4e692054454c6f204f545069204f55522044454c692053414d69204f54526920554e" + "5269205055426920494e4420504859204f4e4c20554e49205055522046494e20434f4d" + "204e415620494e542044454d20434e542053544120504f4c204845412050524520474f" + "56220d0a43616368652d436f6e74726f6c3a20707269766174650d0a566172793a2055" + "7365722d4167656e740d0a5365742d436f6f6b69653a20443d5f796c683d58336f444d" + "54466b64476c6f5a7a567842463954417a49334d5459784e446b4563476c6b417a4578" + "4e6a59794d5463314e5463456447567a64414d7742485274634777446157356b5a5867" + "7462412d2d3b20706174683d2f3b20646f6d61696e3d2e7961686f6f2e636f6d0d0a43" + "6f6e6e656374696f6e3a20636c6f73650d0a5472616e736665722d456e636f64696e67" + "3a206368756e6b65640d0a436f6e74656e742d547970653a20746578742f68746d6c3b" + "20636861727365743d7574662d380d0a436f6e74656e742d456e636f64696e673a2067" + "7a69700d0a0d0a366263382020200d0a1f8b0800000000000003dcbd6977db38b200fa" + "f9fa9cf90f88326dd9b1169212b5d891739cd84ed2936d1277a7d3cbf1a1484a624c91" + "0c4979893bbfec7d7bbfec556121012eb29d65e6be7be7762c9240a1502854150a85c2" + "c37b87af9f9c7c7873449e9dbc7c41defcf2f8c5f327a4d1ee76dff79e74bb872787ec" + "43bfa3e9ddeed1ab06692cd234daed762f2e2e3a17bd4e18cfbb276fbb8b74e9f7bb49" + "1a7b76da7152a7b1bff110dfed3f5cb896030f4b37b508566dbb9f56def9a4f1240c52" + "3748db275791db20367b9a3452f732a5d0f688bdb0e2c44d27bf9c1cb7470830b1632f" + "4a490a3578c18fd6b9c5dec2f7732b2641783109dc0b7268a56e2bd527a931497b93b4" + "3f49cd493a98a4c3493a9aa4e349aa6bf01f7cd78d89d6b2ed49b3d9baf223f8b307b5" + "004a67eea627ded2dddadedb78d8656de428f856305f5973779223b0fff05ebbbde1db" + "67082a499289ae0f06863e1c8f4c0639eaccbdd9a3547abf798a1f0ec6c73fafd2e4f1" + "51ffd5f1c9e2f9e37ff74e74fbddd941b375eadb0942b3e3d5723a69f6060373a6cff4" + "9e6df586dac8b11c4d1f1afd81319b0df45e6fd4925a6cee6db4dbfb19e225bc1b12e5" + "6a098aed9309715c3b74dc5fde3e7f122ea3308061dac22f4018a4f8878367af5f4f2e" + "bcc001a2d187bfffbefeb2477f75026be9269165bb93d92ab0532f0cb68264fbda9b6d" + "dd0b92bfff867f3abe1bccd3c5f675eca6ab3820c1caf7f7be20e05363029f93c8f7d2" + "ad46a7b1bd475ff62614f2de2c8cb7f08537d93a35fed0fe9a4c1af44363fb91beabed" + "790f4f0d0e7a6f67c7dbbe3eedfd01e5bcbffe9a64bf289e00307bb1f7852371dadb13" + "3df0c3798efba9d93a1db44e87dbd7d8b4cf50e95c780e304be745389fbbf11ef4cddf" + "dcf4b162d629fa94d7defbe2fa892b3ece2c78d8fb221a84517003476a73dc3ad535d6" + "e22c7fbd0db8cf3a511ca6211d3e28933fed9d8ea54f381f66c0c7f2cb0e4c3898ad2b" + "3b0de3c9e918bf25abc88d6ddf02d65581418f94174addc9ebe94717e67ce557207b6d" + "45f892773ae393adc62af57c18ecd27b46e5aa2feea5b58c7c173e6d94be1d3bd5afa3" + "fcf571d409ded9b1eb06ef3d275d00c36f25f4916c6ed2a911cef88b0e4c0ecfa7a5b6" + "27936600b3d28d9bdbe411" + )) + + obj = ip_stack.parse(cap1) + print (obj) + print (repr(ip_stack.build(obj))) + + print ("-" * 80) + + obj = ip_stack.parse(cap2) + print (obj) + print (repr(ip_stack.build(obj))) diff --git a/external/construct/protocols/layer2/__init__.py b/external/construct/protocols/layer2/__init__.py new file mode 100644 index 0000000..bdcdb4a --- /dev/null +++ b/external/construct/protocols/layer2/__init__.py @@ -0,0 +1,4 @@ +""" +layer 2 (data link) protocols +""" + diff --git a/external/construct/protocols/layer2/arp.py b/external/construct/protocols/layer2/arp.py new file mode 100644 index 0000000..3e86b53 --- /dev/null +++ b/external/construct/protocols/layer2/arp.py @@ -0,0 +1,94 @@ +""" +Ethernet (TCP/IP protocol stack) +""" +from construct import * +from ethernet import MacAddressAdapter +from construct.protocols.layer3.ipv4 import IpAddressAdapter +from binascii import unhexlify +import six + + + +def HwAddress(name): + return IfThenElse(name, lambda ctx: ctx.hardware_type == "ETHERNET", + MacAddressAdapter(Field("data", lambda ctx: ctx.hwaddr_length)), + Field("data", lambda ctx: ctx.hwaddr_length) + ) + +def ProtoAddress(name): + return IfThenElse(name, lambda ctx: ctx.protocol_type == "IP", + IpAddressAdapter(Field("data", lambda ctx: ctx.protoaddr_length)), + Field("data", lambda ctx: ctx.protoaddr_length) + ) + +arp_header = Struct("arp_header", + Enum(UBInt16("hardware_type"), + ETHERNET = 1, + EXPERIMENTAL_ETHERNET = 2, + ProNET_TOKEN_RING = 4, + CHAOS = 5, + IEEE802 = 6, + ARCNET = 7, + HYPERCHANNEL = 8, + ULTRALINK = 13, + FRAME_RELAY = 15, + FIBRE_CHANNEL = 18, + IEEE1394 = 24, + HIPARP = 28, + ISO7816_3 = 29, + ARPSEC = 30, + IPSEC_TUNNEL = 31, + INFINIBAND = 32, + ), + Enum(UBInt16("protocol_type"), + IP = 0x0800, + ), + UBInt8("hwaddr_length"), + UBInt8("protoaddr_length"), + Enum(UBInt16("opcode"), + REQUEST = 1, + REPLY = 2, + REQUEST_REVERSE = 3, + REPLY_REVERSE = 4, + DRARP_REQUEST = 5, + DRARP_REPLY = 6, + DRARP_ERROR = 7, + InARP_REQUEST = 8, + InARP_REPLY = 9, + ARP_NAK = 10 + + ), + HwAddress("source_hwaddr"), + ProtoAddress("source_protoaddr"), + HwAddress("dest_hwaddr"), + ProtoAddress("dest_protoaddr"), +) + +rarp_header = Rename("rarp_header", arp_header) + + +if __name__ == "__main__": + cap1 = unhexlify(six.b("00010800060400010002e3426009c0a80204000000000000c0a80201")) + obj = arp_header.parse(cap1) + print (obj) + print (repr(arp_header.build(obj))) + + print ("-" * 80) + + cap2 = unhexlify(six.b("00010800060400020011508c283cc0a802010002e3426009c0a80204")) + obj = arp_header.parse(cap2) + print (obj) + print (repr(arp_header.build(obj))) + + + + + + + + + + + + + diff --git a/external/construct/protocols/layer2/ethernet.py b/external/construct/protocols/layer2/ethernet.py new file mode 100644 index 0000000..d8d4bdd --- /dev/null +++ b/external/construct/protocols/layer2/ethernet.py @@ -0,0 +1,38 @@ +""" +Ethernet (TCP/IP protocol stack) +""" +from construct import * +from binascii import hexlify, unhexlify +import six + + +class MacAddressAdapter(Adapter): + def _encode(self, obj, context): + return unhexlify(obj.replace("-", "")) + def _decode(self, obj, context): + return "-".join(hexlify(b) for b in obj) + +def MacAddress(name): + return MacAddressAdapter(Bytes(name, 6)) + +ethernet_header = Struct("ethernet_header", + MacAddress("destination"), + MacAddress("source"), + Enum(UBInt16("type"), + IPv4 = 0x0800, + ARP = 0x0806, + RARP = 0x8035, + X25 = 0x0805, + IPX = 0x8137, + IPv6 = 0x86DD, + _default_ = Pass, + ), +) + + +if __name__ == "__main__": + cap = unhexlify(six.b("0011508c283c0002e34260090800")) + obj = ethernet_header.parse(cap) + print (obj) + print (repr(ethernet_header.build(obj))) + diff --git a/external/construct/protocols/layer2/mtp2.py b/external/construct/protocols/layer2/mtp2.py new file mode 100644 index 0000000..1cbe3a0 --- /dev/null +++ b/external/construct/protocols/layer2/mtp2.py @@ -0,0 +1,21 @@ +""" +Message Transport Part 2 (SS7 protocol stack) +(untested) +""" +from construct import * + + +mtp2_header = BitStruct("mtp2_header", + Octet("flag1"), + Bits("bsn", 7), + Bit("bib"), + Bits("fsn", 7), + Bit("sib"), + Octet("length"), + Octet("service_info"), + Octet("signalling_info"), + Bits("crc", 16), + Octet("flag2"), +) + + diff --git a/external/construct/protocols/layer3/__init__.py b/external/construct/protocols/layer3/__init__.py new file mode 100644 index 0000000..4477713 --- /dev/null +++ b/external/construct/protocols/layer3/__init__.py @@ -0,0 +1,4 @@ +""" +layer 3 (network) protocols +""" + diff --git a/external/construct/protocols/layer3/__init__.pyc b/external/construct/protocols/layer3/__init__.pyc Binary files differnew file mode 100644 index 0000000..5275b6e --- /dev/null +++ b/external/construct/protocols/layer3/__init__.pyc diff --git a/external/construct/protocols/layer3/dhcpv4.py b/external/construct/protocols/layer3/dhcpv4.py new file mode 100644 index 0000000..f39bc97 --- /dev/null +++ b/external/construct/protocols/layer3/dhcpv4.py @@ -0,0 +1,212 @@ +""" +Dynamic Host Configuration Protocol for IPv4 + +http://www.networksorcery.com/enp/protocol/dhcp.htm +http://www.networksorcery.com/enp/protocol/bootp/options.htm +""" +from construct import * +from ipv4 import IpAddress +from binascii import unhexlify +import six + + +dhcp_option = Struct("dhcp_option", + Enum(Byte("code"), + Pad = 0, + Subnet_Mask = 1, + Time_Offset = 2, + Router = 3, + Time_Server = 4, + Name_Server = 5, + Domain_Name_Server = 6, + Log_Server = 7, + Quote_Server = 8, + LPR_Server = 9, + Impress_Server = 10, + Resource_Location_Server = 11, + Host_Name = 12, + Boot_File_Size = 13, + Merit_Dump_File = 14, + Domain_Name = 15, + Swap_Server = 16, + Root_Path = 17, + Extensions_Path = 18, + IP_Forwarding_enabledisable = 19, + Nonlocal_Source_Routing_enabledisable = 20, + Policy_Filter = 21, + Maximum_Datagram_Reassembly_Size = 22, + Default_IP_TTL = 23, + Path_MTU_Aging_Timeout = 24, + Path_MTU_Plateau_Table = 25, + Interface_MTU = 26, + All_Subnets_are_Local = 27, + Broadcast_Address = 28, + Perform_Mask_Discovery = 29, + Mask_supplier = 30, + Perform_router_discovery = 31, + Router_solicitation_address = 32, + Static_routing_table = 33, + Trailer_encapsulation = 34, + ARP_cache_timeout = 35, + Ethernet_encapsulation = 36, + Default_TCP_TTL = 37, + TCP_keepalive_interval = 38, + TCP_keepalive_garbage = 39, + Network_Information_Service_domain = 40, + Network_Information_Servers = 41, + NTP_servers = 42, + Vendor_specific_information = 43, + NetBIOS_over_TCPIP_name_server = 44, + NetBIOS_over_TCPIP_Datagram_Distribution_Server = 45, + NetBIOS_over_TCPIP_Node_Type = 46, + NetBIOS_over_TCPIP_Scope = 47, + X_Window_System_Font_Server = 48, + X_Window_System_Display_Manager = 49, + Requested_IP_Address = 50, + IP_address_lease_time = 51, + Option_overload = 52, + DHCP_message_type = 53, + Server_identifier = 54, + Parameter_request_list = 55, + Message = 56, + Maximum_DHCP_message_size = 57, + Renew_time_value = 58, + Rebinding_time_value = 59, + Class_identifier = 60, + Client_identifier = 61, + NetWareIP_Domain_Name = 62, + NetWareIP_information = 63, + Network_Information_Service_Domain = 64, + Network_Information_Service_Servers = 65, + TFTP_server_name = 66, + Bootfile_name = 67, + Mobile_IP_Home_Agent = 68, + Simple_Mail_Transport_Protocol_Server = 69, + Post_Office_Protocol_Server = 70, + Network_News_Transport_Protocol_Server = 71, + Default_World_Wide_Web_Server = 72, + Default_Finger_Server = 73, + Default_Internet_Relay_Chat_Server = 74, + StreetTalk_Server = 75, + StreetTalk_Directory_Assistance_Server = 76, + User_Class_Information = 77, + SLP_Directory_Agent = 78, + SLP_Service_Scope = 79, + Rapid_Commit = 80, + Fully_Qualified_Domain_Name = 81, + Relay_Agent_Information = 82, + Internet_Storage_Name_Service = 83, + NDS_servers = 85, + NDS_tree_name = 86, + NDS_context = 87, + BCMCS_Controller_Domain_Name_list = 88, + BCMCS_Controller_IPv4_address_list = 89, + Authentication = 90, + Client_last_transaction_time = 91, + Associated_ip = 92, + Client_System_Architecture_Type = 93, + Client_Network_Interface_Identifier = 94, + Lightweight_Directory_Access_Protocol = 95, + Client_Machine_Identifier = 97, + Open_Group_User_Authentication = 98, + Autonomous_System_Number = 109, + NetInfo_Parent_Server_Address = 112, + NetInfo_Parent_Server_Tag = 113, + URL = 114, + Auto_Configure = 116, + Name_Service_Search = 117, + Subnet_Selection = 118, + DNS_domain_search_list = 119, + SIP_Servers_DHCP_Option = 120, + Classless_Static_Route_Option = 121, + CableLabs_Client_Configuration = 122, + GeoConf = 123, + ), + Switch("value", lambda ctx: ctx.code, + { + # codes without any value + "Pad" : Pass, + }, + # codes followed by length and value fields + default = Struct("value", + Byte("length"), + Field("data", lambda ctx: ctx.length), + ) + ) +) + +dhcp_header = Struct("dhcp_header", + Enum(Byte("opcode"), + BootRequest = 1, + BootReply = 2, + ), + Enum(Byte("hardware_type"), + Ethernet = 1, + Experimental_Ethernet = 2, + ProNET_Token_Ring = 4, + Chaos = 5, + IEEE_802 = 6, + ARCNET = 7, + Hyperchannel = 8, + Lanstar = 9, + ), + Byte("hardware_address_length"), + Byte("hop_count"), + UBInt32("transaction_id"), + UBInt16("elapsed_time"), + BitStruct("flags", + Flag("boardcast"), + Padding(15), + ), + IpAddress("client_addr"), + IpAddress("your_addr"), + IpAddress("server_addr"), + IpAddress("gateway_addr"), + IpAddress("client_addr"), + Bytes("client_hardware_addr", 16), + Bytes("server_host_name", 64), + Bytes("boot_filename", 128), + # BOOTP/DHCP options + # "The first four bytes contain the (decimal) values 99, 130, 83 and 99" + Const(Bytes("magic", 4), six.b("\x63\x82\x53\x63")), + Rename("options", OptionalGreedyRange(dhcp_option)), +) + + +if __name__ == "__main__": + test = unhexlify(six.b( + "01" "01" "08" "ff" "11223344" "1234" "0000" + "11223344" "aabbccdd" "11223444" "aabbccdd" "11223344" + + "11223344556677889900aabbccddeeff" + + "41414141414141414141414141414141" "41414141414141414141414141414141" + "41414141414141414141414141414141" "41414141414141414141414141414141" + + "42424242424242424242424242424242" "42424242424242424242424242424242" + "42424242424242424242424242424242" "42424242424242424242424242424242" + "42424242424242424242424242424242" "42424242424242424242424242424242" + "42424242424242424242424242424242" "42424242424242424242424242424242" + + "63825363" + + "0104ffffff00" + "00" + "060811223344aabbccdd" + )) + + print (dhcp_header.parse(test)) + + + + + + + + + + + + + + diff --git a/external/construct/protocols/layer3/dhcpv6.py b/external/construct/protocols/layer3/dhcpv6.py new file mode 100644 index 0000000..ed4f573 --- /dev/null +++ b/external/construct/protocols/layer3/dhcpv6.py @@ -0,0 +1,112 @@ +""" +the Dynamic Host Configuration Protocol (DHCP) for IPv6 + +http://www.networksorcery.com/enp/rfc/rfc3315.txt +""" +from construct import * +from ipv6 import Ipv6Address +import six + + +dhcp_option = Struct("dhcp_option", + Enum(UBInt16("code"), + OPTION_CLIENTID = 1, + OPTION_SERVERID = 2, + OPTION_IA_NA = 3, + OPTION_IA_TA = 4, + OPTION_IAADDR = 5, + OPTION_ORO = 6, + OPTION_PREFERENCE = 7, + OPTION_ELAPSED_TIME = 8, + OPTION_RELAY_MSG = 9, + OPTION_AUTH = 11, + OPTION_UNICAST = 12, + OPTION_STATUS_CODE = 13, + OPTION_RAPID_COMMIT = 14, + OPTION_USER_CLASS = 15, + OPTION_VENDOR_CLASS = 16, + OPTION_VENDOR_OPTS = 17, + OPTION_INTERFACE_ID = 18, + OPTION_RECONF_MSG = 19, + OPTION_RECONF_ACCEPT = 20, + SIP_SERVERS_DOMAIN_NAME_LIST = 21, + SIP_SERVERS_IPV6_ADDRESS_LIST = 22, + DNS_RECURSIVE_NAME_SERVER = 23, + DOMAIN_SEARCH_LIST = 24, + OPTION_IA_PD = 25, + OPTION_IAPREFIX = 26, + OPTION_NIS_SERVERS = 27, + OPTION_NISP_SERVERS = 28, + OPTION_NIS_DOMAIN_NAME = 29, + OPTION_NISP_DOMAIN_NAME = 30, + SNTP_SERVER_LIST = 31, + INFORMATION_REFRESH_TIME = 32, + BCMCS_CONTROLLER_DOMAIN_NAME_LIST = 33, + BCMCS_CONTROLLER_IPV6_ADDRESS_LIST = 34, + OPTION_GEOCONF_CIVIC = 36, + OPTION_REMOTE_ID = 37, + RELAY_AGENT_SUBSCRIBER_ID = 38, + OPTION_CLIENT_FQDN = 39, + ), + UBInt16("length"), + Field("data", lambda ctx: ctx.length), +) + +client_message = Struct("client_message", + Bitwise(BitField("transaction_id", 24)), +) + +relay_message = Struct("relay_message", + Byte("hop_count"), + Ipv6Address("linkaddr"), + Ipv6Address("peeraddr"), +) + +dhcp_message = Struct("dhcp_message", + Enum(Byte("msgtype"), + # these are client-server messages + SOLICIT = 1, + ADVERTISE = 2, + REQUEST = 3, + CONFIRM = 4, + RENEW = 5, + REBIND = 6, + REPLY = 7, + RELEASE_ = 8, + DECLINE_ = 9, + RECONFIGURE = 10, + INFORMATION_REQUEST = 11, + # these two are relay messages + RELAY_FORW = 12, + RELAY_REPL = 13, + ), + # relay messages have a different structure from client-server messages + Switch("params", lambda ctx: ctx.msgtype, + { + "RELAY_FORW" : relay_message, + "RELAY_REPL" : relay_message, + }, + default = client_message, + ), + Rename("options", GreedyRange(dhcp_option)), +) + + +if __name__ == "__main__": + test1 = six.b("\x03\x11\x22\x33\x00\x17\x00\x03ABC\x00\x05\x00\x05HELLO") + test2 = six.b("\x0c\x040123456789abcdef0123456789abcdef\x00\x09\x00\x0bhello world\x00\x01\x00\x00") + print (dhcp_message.parse(test1)) + print (dhcp_message.parse(test2)) + + + + + + + + + + + + + diff --git a/external/construct/protocols/layer3/icmpv4.py b/external/construct/protocols/layer3/icmpv4.py new file mode 100644 index 0000000..2c0715e --- /dev/null +++ b/external/construct/protocols/layer3/icmpv4.py @@ -0,0 +1,98 @@ +""" +Internet Control Message Protocol for IPv4 (TCP/IP protocol stack) +""" +from construct import * +from ipv4 import IpAddress +from binascii import unhexlify +import six + + +echo_payload = Struct("echo_payload", + UBInt16("identifier"), + UBInt16("sequence"), + Bytes("data", 32), # length is implementation dependent... + # is anyone using more than 32 bytes? +) + +dest_unreachable_payload = Struct("dest_unreachable_payload", + Padding(2), + UBInt16("next_hop_mtu"), + IpAddress("host"), + Bytes("echo", 8), +) + +dest_unreachable_code = Enum(Byte("code"), + Network_unreachable_error = 0, + Host_unreachable_error = 1, + Protocol_unreachable_error = 2, + Port_unreachable_error = 3, + The_datagram_is_too_big = 4, + Source_route_failed_error = 5, + Destination_network_unknown_error = 6, + Destination_host_unknown_error = 7, + Source_host_isolated_error = 8, + Desination_administratively_prohibited = 9, + Host_administratively_prohibited2 = 10, + Network_TOS_unreachable = 11, + Host_TOS_unreachable = 12, +) + +icmp_header = Struct("icmp_header", + Enum(Byte("type"), + Echo_reply = 0, + Destination_unreachable = 3, + Source_quench = 4, + Redirect = 5, + Alternate_host_address = 6, + Echo_request = 8, + Router_advertisement = 9, + Router_solicitation = 10, + Time_exceeded = 11, + Parameter_problem = 12, + Timestamp_request = 13, + Timestamp_reply = 14, + Information_request = 15, + Information_reply = 16, + Address_mask_request = 17, + Address_mask_reply = 18, + _default_ = Pass, + ), + Switch("code", lambda ctx: ctx.type, + { + "Destination_unreachable" : dest_unreachable_code, + }, + default = Byte("code"), + ), + UBInt16("crc"), + Switch("payload", lambda ctx: ctx.type, + { + "Echo_reply" : echo_payload, + "Echo_request" : echo_payload, + "Destination_unreachable" : dest_unreachable_payload, + }, + default = Pass + ) +) + + +if __name__ == "__main__": + cap1 = unhexlify(six.b("0800305c02001b006162636465666768696a6b6c6d6e6f70717273747576776162" + "63646566676869")) + cap2 = unhexlify(six.b("0000385c02001b006162636465666768696a6b6c6d6e6f70717273747576776162" + "63646566676869")) + cap3 = unhexlify(six.b("0301000000001122aabbccdd0102030405060708")) + + print (icmp_header.parse(cap1)) + print (icmp_header.parse(cap2)) + print (icmp_header.parse(cap3)) + + + + + + + + + + + diff --git a/external/construct/protocols/layer3/igmpv2.py b/external/construct/protocols/layer3/igmpv2.py new file mode 100644 index 0000000..41797eb --- /dev/null +++ b/external/construct/protocols/layer3/igmpv2.py @@ -0,0 +1,29 @@ +""" +What : Internet Group Management Protocol, Version 2 + How : http://www.ietf.org/rfc/rfc2236.txt + Who : jesse @ housejunkie . ca +""" + +from construct import Byte, Enum,Struct, UBInt16 +from construct.protocols.layer3.ipv4 import IpAddress +from binascii import unhexlify +import six + + +igmp_type = Enum(Byte("igmp_type"), + MEMBERSHIP_QUERY = 0x11, + MEMBERSHIP_REPORT_V1 = 0x12, + MEMBERSHIP_REPORT_V2 = 0x16, + LEAVE_GROUP = 0x17, +) + +igmpv2_header = Struct("igmpv2_header", + igmp_type, + Byte("max_resp_time"), + UBInt16("checksum"), + IpAddress("group_address"), +) + +if __name__ == '__main__': + capture = unhexlify(six.b("1600FA01EFFFFFFD")) + print (igmpv2_header.parse(capture)) diff --git a/external/construct/protocols/layer3/ipv4.py b/external/construct/protocols/layer3/ipv4.py new file mode 100644 index 0000000..82dfaa9 --- /dev/null +++ b/external/construct/protocols/layer3/ipv4.py @@ -0,0 +1,85 @@ +""" +Internet Protocol version 4 (TCP/IP protocol stack) +""" +from construct import * +import six +from binascii import unhexlify + +try: + bytes +except NameError: + bytes = str + + +class IpAddressAdapter(Adapter): + def _encode(self, obj, context): + if bytes is str: + return "".join(chr(int(b)) for b in obj.split(".")) + else: + return bytes(int(b) for b in obj.split(".")) + def _decode(self, obj, context): + if bytes is str: + return ".".join(str(ord(b)) for b in obj) + else: + return ".".join("%d" % (b,) for b in obj) + +def IpAddress(name): + return IpAddressAdapter(Bytes(name, 4)) + +def ProtocolEnum(code): + return Enum(code, + ICMP = 1, + TCP = 6, + UDP = 17, + ) + +ipv4_header = Struct("ip_header", + EmbeddedBitStruct( + Const(Nibble("version"), 4), + ExprAdapter(Nibble("header_length"), + decoder = lambda obj, ctx: obj * 4, + encoder = lambda obj, ctx: obj / 4 + ), + ), + BitStruct("tos", + Bits("precedence", 3), + Flag("minimize_delay"), + Flag("high_throuput"), + Flag("high_reliability"), + Flag("minimize_cost"), + Padding(1), + ), + UBInt16("total_length"), + Value("payload_length", lambda ctx: ctx.total_length - ctx.header_length), + UBInt16("identification"), + EmbeddedBitStruct( + Struct("flags", + Padding(1), + Flag("dont_fragment"), + Flag("more_fragments"), + ), + Bits("frame_offset", 13), + ), + UBInt8("ttl"), + ProtocolEnum(UBInt8("protocol")), + UBInt16("checksum"), + IpAddress("source"), + IpAddress("destination"), + Field("options", lambda ctx: ctx.header_length - 20), +) + + +if __name__ == "__main__": + cap = unhexlify(six.b("4500003ca0e3000080116185c0a80205d474a126")) + obj = ipv4_header.parse(cap) + print (obj) + print (repr(ipv4_header.build(obj))) + + + + + + + + + diff --git a/external/construct/protocols/layer3/ipv4.pyc b/external/construct/protocols/layer3/ipv4.pyc Binary files differnew file mode 100644 index 0000000..cc4fbbf --- /dev/null +++ b/external/construct/protocols/layer3/ipv4.pyc diff --git a/external/construct/protocols/layer3/ipv6.py b/external/construct/protocols/layer3/ipv6.py new file mode 100644 index 0000000..18a0955 --- /dev/null +++ b/external/construct/protocols/layer3/ipv6.py @@ -0,0 +1,52 @@ +""" +Internet Protocol version 6 (TCP/IP protocol stack) +""" +from construct import * +from ipv4 import ProtocolEnum +from binascii import unhexlify +import six + + +class Ipv6AddressAdapter(Adapter): + def _encode(self, obj, context): + if bytes is str: + return "".join(part.decode("hex") for part in obj.split(":")) + else: + return bytes(int(part, 16) for part in obj.split(":")) + def _decode(self, obj, context): + if bytes is str: + return ":".join(b.encode("hex") for b in obj) + else: + return ":".join("%02x" % (b,) for b in obj) + +def Ipv6Address(name): + return Ipv6AddressAdapter(Bytes(name, 16)) + + +ipv6_header = Struct("ip_header", + EmbeddedBitStruct( + OneOf(Bits("version", 4), [6]), + Bits("traffic_class", 8), + Bits("flow_label", 20), + ), + UBInt16("payload_length"), + ProtocolEnum(UBInt8("protocol")), + UBInt8("hoplimit"), + Alias("ttl", "hoplimit"), + Ipv6Address("source"), + Ipv6Address("destination"), +) + + +if __name__ == "__main__": + o = ipv6_header.parse(six.b("\x6f\xf0\x00\x00\x01\x02\x06\x80" + "0123456789ABCDEF" "FEDCBA9876543210" + )) + print (o) + print (repr(ipv6_header.build(o))) + + + + + + diff --git a/external/construct/protocols/layer3/mtp3.py b/external/construct/protocols/layer3/mtp3.py new file mode 100644 index 0000000..7f712f2 --- /dev/null +++ b/external/construct/protocols/layer3/mtp3.py @@ -0,0 +1,12 @@ +""" +Message Transport Part 3 (SS7 protocol stack) +(untested) +""" +from construct import * + + +mtp3_header = BitStruct("mtp3_header", + Nibble("service_indicator"), + Nibble("subservice"), +) + diff --git a/external/construct/protocols/layer4/__init__.py b/external/construct/protocols/layer4/__init__.py new file mode 100644 index 0000000..38693c6 --- /dev/null +++ b/external/construct/protocols/layer4/__init__.py @@ -0,0 +1,4 @@ +""" +layer 4 (transporation) protocols +""" + diff --git a/external/construct/protocols/layer4/isup.py b/external/construct/protocols/layer4/isup.py new file mode 100644 index 0000000..8111b60 --- /dev/null +++ b/external/construct/protocols/layer4/isup.py @@ -0,0 +1,15 @@ +""" +ISDN User Part (SS7 protocol stack) +""" +from construct import * + + +isup_header = Struct("isup_header", + Bytes("routing_label", 5), + UBInt16("cic"), + UBInt8("message_type"), + # mandatory fixed parameters + # mandatory variable parameters + # optional parameters +) + diff --git a/external/construct/protocols/layer4/tcp.py b/external/construct/protocols/layer4/tcp.py new file mode 100644 index 0000000..44f1bd7 --- /dev/null +++ b/external/construct/protocols/layer4/tcp.py @@ -0,0 +1,61 @@ +""" +Transmission Control Protocol (TCP/IP protocol stack) +""" +from construct import * +from binascii import unhexlify +import six + + +tcp_header = Struct("tcp_header", + UBInt16("source"), + UBInt16("destination"), + UBInt32("seq"), + UBInt32("ack"), + EmbeddedBitStruct( + ExprAdapter(Nibble("header_length"), + encoder = lambda obj, ctx: obj / 4, + decoder = lambda obj, ctx: obj * 4, + ), + Padding(3), + Struct("flags", + Flag("ns"), + Flag("cwr"), + Flag("ece"), + Flag("urg"), + Flag("ack"), + Flag("psh"), + Flag("rst"), + Flag("syn"), + Flag("fin"), + ), + ), + UBInt16("window"), + UBInt16("checksum"), + UBInt16("urgent"), + Field("options", lambda ctx: ctx.header_length - 20), +) + +if __name__ == "__main__": + cap = unhexlify(six.b("0db5005062303fb21836e9e650184470c9bc0000")) + + obj = tcp_header.parse(cap) + print (obj) + built = tcp_header.build(obj) + print (built) + assert cap == built + + + + + + + + + + + + + + + + diff --git a/external/construct/protocols/layer4/udp.py b/external/construct/protocols/layer4/udp.py new file mode 100644 index 0000000..42a951e --- /dev/null +++ b/external/construct/protocols/layer4/udp.py @@ -0,0 +1,26 @@ +""" +User Datagram Protocol (TCP/IP protocol stack) +""" +from construct import * +import six +from binascii import unhexlify + + +udp_header = Struct("udp_header", + Value("header_length", lambda ctx: 8), + UBInt16("source"), + UBInt16("destination"), + ExprAdapter(UBInt16("payload_length"), + encoder = lambda obj, ctx: obj + 8, + decoder = lambda obj, ctx: obj - 8, + ), + UBInt16("checksum"), +) + +if __name__ == "__main__": + cap = unhexlify(six.b("0bcc003500280689")) + obj = udp_header.parse(cap) + print (obj) + print (repr(udp_header.build(obj))) + + diff --git a/external/construct/version.py b/external/construct/version.py new file mode 100644 index 0000000..df1db1f --- /dev/null +++ b/external/construct/version.py @@ -0,0 +1,3 @@ +version = (2, 5, 2) +version_string = "2.5.2" +release_date = "2014.04.28" diff --git a/external/construct/version.pyc b/external/construct/version.pyc Binary files differnew file mode 100644 index 0000000..dfda228 --- /dev/null +++ b/external/construct/version.pyc diff --git a/external/plyer/CHANGELOG.md b/external/plyer/CHANGELOG.md new file mode 100644 index 0000000..20f5bcf --- /dev/null +++ b/external/plyer/CHANGELOG.md @@ -0,0 +1,939 @@ +Changelog +========= + +%%version%% (unreleased) +------------------------ + +- Update __init__.py. [dessant] + +- Update copyright year. [dessant] + +- Fix windows filechooser using `mode` instead of `self.mode` [gabriel + pettier] + +- Merge pull request #144 from thegrymek/plyer-audio. [dessant] + + Plyer audio for android with facade and example + +- Update docs facade with permissions. [andrzej.grymkowski] + +- Improving docs. [andrzej.grymkowski] + +- Corrected "" in docstring. [Andrzej Grymkowski] + +- Flake8. [Andrzej Grymkowski] + +- Update readme. [Andrzej Grymkowski] + +- Removing temporary files. [Andrzej Grymkowski] + +- Moved example class to facades. [Andrzej Grymkowski] + +- Impoved example. [andrzej.grymkowski] + +- Added almost working example. [Andrzej Grymkowski] + +- Initial commit. [Andrzej Grymkowski] + +- Merge pull request #147 from kivy/stylefix. [dessant] + + style fixes + +- Style fixes. [dessant] + +- Merge pull request #146 from kivy/dessant-patch-1. [dessant] + + add pydev files to gitignore + +- Add pydev files to gitignore. [dessant] + +- Merge pull request #145 from thegrymek/plyer-style-guide-update. + [dessant] + + Plyer style guide update + +- Another improvements. [andrzej.grymkowski] + +- Corrected win filechooser. [andrzej.grymkowski] + +- Update for docs. [andrzej.grymkowski] + +- Improved styles in files. [andrzej.grymkowski] + +- Updated kivy pep8 and corrected some files. [andrzej.grymkowski] + +- Merge pull request #143 from kivy/dessant-patch-1. [dessant] + + fix versionchanged tag + +- Fix versionchanged tag. [dessant] + +- Merge pull request #133 from aron-bordin/master. [dessant] + + fix #107 - Use Android_ID instead of IMEI + +- Fix #107 - Use Android_ID instead of IMEI. [Aron Bordin] + +- Merge pull request #140 from thegrymek/plyer-android4.0-email. + [dessant] + + update info about support email for android<4.0 + +- Update info to readme about support email on android<4.0. + [andrzej.grymkowski] + +- Merge pull request #135 from thegrymek/inclement-orientation. + [dessant] + + Inclement orientation + +- Adding tag versionadded to facade. [andrzej.grymkowski] + +- Fix for import facade orientation. [Andrzej Grymkowski] + +- Added to readme supported platforms platform android < 4.0. [Andrzej + Grymkowski] + +- Removing red functions label. [Andrzej Grymkowski] + +- Corrected readme. [Andrzej Grymkowski] + +- Updated orientation. [Andrzej Grymkowski] + +- Merge pull request #139 from thegrymek/plyer-add-package-facades- + setuptools. [dessant] + + added plyer.facade to setuptools package + +- Added plyer.facade to setuptools package. [Andrzej Grymkowski] + +- Merge pull request #138 from thegrymek/plyer-split-facades. [dessant] + + splitted facades + +- Changed order. [Andrzej Grymkowski] + +- Spliting end. [Andrzej Grymkowski] + +- Spliting end. [Andrzej Grymkowski] + +- First part. [Andrzej Grymkowski] + +- Merge pull request #129 from thegrymek/android-vibrator-sdk-9. + [dessant] + + vibrator for android v < 4.0 + +- Added defaults. [Andrzej Grymkowski] + +- Removing files tests. [Andrzej Grymkowski] + +- A. [Andrzej Grymkowski] + +- Merging. [Andrzej Grymkowski] + +- Replace """ in docstrings" [Andrzej Grymkowski] + +- Added exist for sdk < 11. [Andrzej Grymkowski] + +- Added unsupported exception. [Andrzej Grymkowski] + +- Removed unnessesary docstrings. [Andrzej Grymkowski] + +- Conflicts main. [Andrzej Grymkowski] + +- Updated document. [Andrzej Grymkowski] + +- Vibrator for android v < 4.0, pep257 and flake8. [Andrzej Grymkowski] + +- Updated document. [Andrzej Grymkowski] + +- Remove unnessesary docstring. [Andrzej Grymkowski] + +- Merging. [Andrzej Grymkowski] + +- Merge branch 'new_branch_name' [Andrzej Grymkowski] + +- Merge. [Andrzej Grymkowski] + +- Remove unused variables. [thegrymek] + +- PEP8 and typo fixes in MacOS X file chooser. [Robert Jerovsek] + +- Removed unused imports and refactor. [Andrzej Grymkowski] + +- Pep8 - removed unused imports and variables. [Andrzej Grymkowski] + +- Make pep8 compatible. [laltin] + +- Responds to issue 109 https://github.com/kivy/plyer/issues/109. + [albericc] + +- As. [Andrzej Grymkowski] + +- Added first structure. [Andrzej Grymkowski] + +- Merge pull request #1 from kivy/master. [thegrymek] + + sync pull request + +- Merge pull request #134 from thegrymek/patch-1. [dessant] + + remove unused variables + +- Remove unused variables. [thegrymek] + +- Merge pull request #110 from AlbericC/issue_109. [dessant] + + responds to issue 109 https://github.com/kivy/plyer/issues/109 + +- Responds to issue 109 https://github.com/kivy/plyer/issues/109. + [albericc] + +- Merge pull request #121 from laltin/pep8_fix. [Mathieu Virbel] + + Pep8 fix + +- Make pep8 compatible. [laltin] + +- Merge pull request #1 from kivy/master. [Lütfi Altın] + + sync with origin + +- Merge pull request #122 from thegrymek/pep8-removed-unused-imports- + and-variables. [Mathieu Virbel] + + pep8 - removed unused imports and variables + +- Removed unused imports and refactor. [Andrzej Grymkowski] + +- Pep8 - removed unused imports and variables. [Andrzej Grymkowski] + +- Merge pull request #123 from robertjerovsek/master. [Mathieu Virbel] + + PEP8 and typo fixes in MacOS X file chooser. + +- PEP8 and typo fixes in MacOS X file chooser. [Robert Jerovsek] + +- Merge pull request #106 from Davideddu/filechooser. [Mathieu Virbel] + + Add file chooser facade and support for Linux and Windows + +- Fix filter conversion on Mac OS X. [Davide Depau] + +- Add experimental support for Mac OS X. [Davide Depau] + +- Fix inheritance issue on windows filechooser implementation. [Davide + Depau] + +- Add filechooser facade and support for Linux and Windows. [Davide + Depau] + +- Merge pull request #119 from trivedigaurav/use-environ-lang. [Mathieu + Virbel] + + Use environ to change LANG to 'C' while calling shell processes + +- Use environ to change LANG to 'C' while calling shell processes. + [gtrivedi] + +- Merge pull request #120 from pspchucky/master. [Mathieu Virbel] + + add video recoding to Camera facade and camera.py + +- Update camera.py. [pspchucky] + +- Add video capture support to facades.py. [pspchucky] + +- Merge pull request #118 from kived/irblaster-facade. [Akshay Arora] + + add IrBlaster facade and Android implementation + +- Add IrBlaster facade and Android implementation. [Ryan Pessa] + +- Update README.rst. [Mathieu Virbel] + +- Merge pull request #116 from laltin/ios_gps. [Mathieu Virbel] + + iOS GPS support + +- Cleanup & comment. [laltin] + +- First working version. [laltin] + +- Merge pull request #115 from kivy/fixes_114. [Mathieu Virbel] + + use environ to change LANG to 'C' while calling lshw + +- Use environ to change LANG to 'C' while calling lshw. [gabriel + pettier] + + fixes #114 + +- Merge pull request #117 from JimmyStavros/patch-1. [Mathieu Virbel] + + Android gps.py: fixed location provider cycling + +- Android gps.py: fixed location provider cycling. [JimmyStavros] + + Issue was first brought to light here: https://github.com/kivy/plyer/issues/54 + + GPS module was cycling through "gps" three times instead of all location services. + +- Fixes hashCode overflow. Latest kivy/pyjnius master have already + hashCode/equals/toString implementation, and the hashCode is fixed + with overflow. Just dont try to implement it here. Closes #103. Ref + kivy/pyjnius#146. [Mathieu Virbel] + +- Bump to 1.2.4-dev. [Mathieu Virbel] + +1.2.3 (2015-01-27) +------------------ + +- Bump to 1.2.3. [Mathieu Virbel] + +- Setup.py: add changelog into the description + fix rst issue. [Mathieu + Virbel] + +- Bump to 1.2.3-dev. [Mathieu Virbel] + +1.2.2 (2015-01-27) +------------------ + +- Bump to 1.2.2. [Mathieu Virbel] + +- Update the version to the next dev (missing from the last release) + [Mathieu Virbel] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Merge pull request #102 from helenst/sys-platform-linux-check. + [Mathieu Virbel] + + Linux platform check made compatible with python 3.3+ (Fixes #58) + +- Linux platform check made compatible with python 3.3+ (Fixes #58) + [Helen ST] + +- Add initial changelog. [Mathieu Virbel] + +- Plyer: fix androidd notification. Closes #93 (credits to @kashifpk) + [Mathieu Virbel] + +- Android/notification: implement a switch to allow usage of API < 16. + [Mathieu Virbel] + +1.2.1 (2014-08-19) +------------------ + +- Merge pull request #92 from dessant/patch-2. [trivedigaurav] + + fix print statement + +- Fix print statement. [dessant] + +- SMS Manager is supported since Android 1.6. [trivedigaurav] + +- Merge pull request #90 from trivedigaurav/ios_uuid. [trivedigaurav] + + iOS UUID facade + +- IOS UUID facade. [gtrivedi] + +- Merge pull request #86 from trivedigaurav/ios_battery. [trivedigaurav] + + iOS Battery + +- Change get_status to get_state. [gtrivedi] + +- Clean up. [gtrivedi] + +- Add battery.py. [gtrivedi] + +- IOS Battery Facade. [gtrivedi] + +- Update compass.py. [trivedigaurav] + +- Update gyroscope.py. [trivedigaurav] + +- Fix typo. [trivedigaurav] + +- Fix typo. [trivedigaurav] + +- Fix style. [gtrivedi] + +- Merge branch 'trivedigaurav-ios_tts' [gtrivedi] + +- Fixing README confict and merge master. [gtrivedi] + +- Merge pull request #88 from trivedigaurav/ios_email. [trivedigaurav] + + iOS Email Facade + +- Add email.py. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into ios_email. + [gtrivedi] + +- Merge pull request #89 from trivedigaurav/fix_make. [trivedigaurav] + + Removing build_ext from plyer + +- Removing build_ext from plyer. [gtrivedi] + +- Update accelerometer.py. [trivedigaurav] + +- Python 3 compat. [trivedigaurav] + +- Python 3 compat. [trivedigaurav] + +- Python 3 compat. [trivedigaurav] + +- IOS Email Facade. [gtrivedi] + +- Merge pull request #82 from trivedigaurav/sensors_start_none. + [trivedigaurav] + + Fix Android enable and disable. Return (None, None, None) until sensor data is available + +- Fix sensor enable-disable. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into + sensors_start_none. [gtrivedi] + +- Merge pull request #68 from trivedigaurav/linux_accel. [trivedigaurav] + + Linux accelerometer facade + +- Linux accelerometer facade. [gtrivedi] + +- Fix style error. [trivedigaurav] + +- Merge pull request #85 from trivedigaurav/battery_ischarging. + [trivedigaurav] + + Change connected to isCharging + +- Change connected to isCharging. [gtrivedi] + +- Merge pull request #80 from ChrisCole42/patch-2. [trivedigaurav] + + Update compass.py + +- Update compass.py. [ChrisCole42] + + copied the missing definitions across from accelerometer.py + +- Merge pull request #79 from trivedigaurav/where_is. [trivedigaurav] + + Use whereis_exe to check for binaries + +- Remove trailing newline. [gtrivedi] + +- Add whereis_exe import. [gtrivedi] + +- Remove trailing newline. [gtrivedi] + +- Use whereis_exe to check for binaries. [gtrivedi] + +- Fix style errors. [gtrivedi] + +- Copy values atomically. [gtrivedi] + +- Return None untill data is available. [gtrivedi] + +- Merge pull request #77 from ChrisCole42/patch-1. [trivedigaurav] + + Update compass.py + +- Update compass.py. [ChrisCole42] + + fix "global name is not defined" error in AndroidCompass.__init__() + +- Merge pull request #75 from trivedigaurav/maintenance. [trivedigaurav] + + Maintenance merge + +- Remove extra make command. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into + maintenance. [gtrivedi] + + Conflicts: + plyer/facades.py + +- Changed battery Xs to correct columns (ios -> win) [Alexander Taylor] + +- Really did fix battery formatting in readme. [Alexander Taylor] + +- Fixed battery formatting in readme. [Alexander Taylor] + +- Merge pull request #74 from dessant/patch-1. [Akshay Arora] + + facade docstring revision + +- Facade docstring revision. [dessant] + +- Merge pull request #73 from trivedigaurav/battery_info. [Akshay Arora] + + Query Battery info/status + +- Merge pull request #71 from trivedigaurav/master. [trivedigaurav] + + Revert "Activity was imported twice" + +- Revert "Activity was imported twice" [gtrivedi] + + This reverts commit a0600929774c1e90c7dc43043ff87b5ea84213b4. + +- Activity was imported twice. [trivedigaurav] + +- Fix style errors after merging with master. [gtrivedi] + +- Fix styles errors in libs/ [gtrivedi] + +- Don't ignore lib folder. [gtrivedi] + +- Fix style errors. [gtrivedi] + +- Fix docstrings. [gtrivedi] + +- Fix typo and style. [gtrivedi] + +- Copy pep8 style checker from Kivy. [gtrivedi] + +- Add Makefile. [gtrivedi] + +- Add windows implementation. [gtrivedi] + +- Fix android implemenation typos. [gtrivedi] + +- Battery facade. [gtrivedi] + +- Merge branch 'master' of https://github.com/kivy/plyer into + battery_info. [gtrivedi] + + Conflicts: + plyer/__init__.py + +- Merge pull request #70 from trivedigaurav/master. [trivedigaurav] + + Fix tabbing + +- Merge pull request #69 from trivedigaurav/gyroscope_fix. + [trivedigaurav] + + Gyroscope facade proxy declarations + +- Gyroscope facade proxy declarations. [gtrivedi] + +- Merge pull request #67 from trivedigaurav/patch-1. [Akshay Arora] + + Update README.rst + +- Update README.rst. [trivedigaurav] + + We have merged commits for the email facades on desktops. + +- Merge branch 'master' of https://github.com/trivedigaurav/plyer into + battery_info. [gtrivedi] + +- Fix tabbing. [trivedigaurav] + + Not sure how this passed through. + +- Battery facade. [gtrivedi] + +- Typo. [Mathieu Virbel] + +- Ios: gyroscope is also supported now. [Mathieu Virbel] + +1.2.0 (2014-06-24) +------------------ + +- Bump to 1.2.0, and mark new classes to 1.2.0. [Mathieu Virbel] + +- Merge master. [Mathieu Virbel] + +- Remove dependency on Hardware.java. [gtrivedi] + +- IOS and Android implementations. [gtrivedi] + +- Gyroscope facade. [gtrivedi] + +- Add gyroscope in README. [gtrivedi] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Merge pull request #49 from Davideddu/macosx_email. [Mathieu Virbel] + + add Mac OS X email support + +- Add Mac OS X email support. [Davide Depau] + +- Merge pull request #48 from Davideddu/windows_email. [Mathieu Virbel] + + add Windows email support + +- Catch exception if no email client is installed on Windows. [Davide + Depau] + +- Add Windows email support. [Davide Depau] + +- Merge pull request #47 from Davideddu/linux_email. [Mathieu Virbel] + + added Linux email support + +- Revert previous commit, use xdg-open to open email client. [Davide + Depau] + +- Use xdg-email by default, fallback to xdg-open + mailto uri. [Davide + Depau] + +- Added Linux email support. [Davide Depau] + +- Merge master. [Mathieu Virbel] + +- Remove gyroscope files. [gtrivedi] + +- Fix typos introduced while copying. [gtrivedi] + +- Fix typo. [gtrivedi] + +- Fix tabbing. [gtrivedi] + +- IOS Magnetometer. [gtrivedi] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Merge pull request #63 from trivedigaurav/pyjnius_accel. [Mathieu + Virbel] + + Switched to pyjnius + +- Make values assignment atomic. [gtrivedi] + +- Switched to pyjnius. [gtrivedi] + +- Merge master. [Mathieu Virbel] + +- Plyer Unique ID facade. [gtrivedi] + +- Merge master. [Mathieu Virbel] + +- Update README. [trivedigaurav] + +- Merge branch 'android_compass' of + https://github.com/trivedigaurav/plyer into android_compass. + [gtrivedi] + +- Update buildozer.spec. [trivedigaurav] + +- Pyjnius compass. [gtrivedi] + +- Update buildozer.spec. [gtrivedi] + +- Plyer compass facade. [gtrivedi] + +- Merge pull request #52 from mihaineacsu/sms. [Mathieu Virbel] + + Added sms facade, example and android implementation + +- Updated sms components. [mihaineacsu] + +- Fix components of the sms facade. [mihaineacsu] + +- Update README with sms feature. [mihaineacsu] + +- Update plyer init. [mihaineacsu] + +- Add sms example. [mihaineacsu] + +- Add initial android sms implementation. [mihaineacsu] + +- Add sms facade. [mihaineacsu] + +- Merge pull request #55 from trivedigaurav/osx_accel. [Mathieu Virbel] + + Using sudden motion sensor as accelerometer on OSX + +- Check accelerometer in OSX. [gtrivedi] + +- LGPL notice no longer required. [gtrivedi] + +- Fix tabs to spaces. [gtrivedi] + +- Switched to ctypes. [gtrivedi] + +- Remove osx libs in setup.py. [gtrivedi] + +- Added LGPL.txt for unimotion.c. [gtrivedi] + +- Added libs/ folder in macosx. [gtrivedi] + +- OSX Accel facade using unimotion. [gtrivedi] + +- Merge pull request #62 from trivedigaurav/patch-2. [Mathieu Virbel] + + Update README + +- Update README. [trivedigaurav] + +- Merge pull request #56 from trivedigaurav/patch-1. [Akshay Arora] + + Update README + +- Update README. [trivedigaurav] + +- Remove buildozer db. [Mathieu Virbel] + +- Merge pull request #46 from matham/master. [akshayaurora] + + Add compat module, remove decoding of strings in notification + +- Explicitly declare HANDLE since ctypes in py3 doesn't seem to have it. + [Matthew Einhorn] + +- Make py3 compat. [Matthew Einhorn] + +- Decode textinput input before sending to notification. [Matthew + Einhorn] + +- Add compat module, remove decoding of strings in notification, do + direct inmport instead of relative import. [Matthew Einhorn] + +- Removed unused import. [Ben Rousch] + +- Merge pull request #6 from inclement/vibrate. [Alexander Taylor] + + Added Vibrator facade and android implementation + +- Renamed vibrate_pattern to just pattern. [Alexander Taylor] + +- Fixed typo in vibrator proxy comment. [Alexander Taylor] + +- Added vibrator example and buildozer.spec. [Alexander Taylor] + +- Added AndroidVibrator implementation. [Alexander Taylor] + +- Added Vibrator facade. [Alexander Taylor] + +- Merge pull request #18 from matham/ctypes-notify-window. [Ben Rousch] + + Changes notify to use ctypes instead of win32gui so we could use unicode. + +- Use unique id. [Matthew Einhorn] + +- Use count instead of self to generate different classes. [Matthew + Einhorn] + +- Add notify_close method and always display some systray icon. [Matthew + Einhorn] + +- Update to use ctypes instead of win32gui so we could use unicode. + [Matthew Einhorn] + +- Add windows ctypes api defs file. [Matthew Einhorn] + +- Merge pull request #39 from trivedigaurav/accelerometer_example. [Ben + Rousch] + + Created an accelerometer example. Uses garden graph to plot the values + +- Removed unused variable. [gtrivedi] + +- Added a simple accelerometer example. [gtrivedi] + +- Corrected README for buildozer commands. [gtrivedi] + +- Updated README to reflect path changes. [gtrivedi] + +- Moved the graph example to examples/accelerometer/using_graph. + [gtrivedi] + +- Updated README to include information about garden.graph. [gtrivedi] + +- Added screenshot of the app running on Android 4.3. [gtrivedi] + +- Added README on how to build. [gtrivedi] + +- Cleaned up a bit. Newlines. Removed dangling commented lines. + [gtrivedi] + +- Fixed ordering in the try catch block. It fails at the correct line in + the try block if the accelerometer is not present. [gtrivedi] + +- Fixed android permissions. Doesn't require any. [gtrivedi] + +- Changed popup error message to state that the problem is with the + current platform. [gtrivedi] + +- Removed a test file not a part of the example. [gtrivedi] + +- Created an accelerometer example. Uses garden graph to plot the + values. [gtrivedi] + +- Added examples README. [Ben Rousch] + +- Merge pull request #38 from trivedigaurav/tts_example. [Ben Rousch] + + Shows an error popup if there is no TTS + +- Added newlines at the end of files. [gtrivedi] + +- Shows an error popup if there is no TTS capability. [gtrivedi] + +- Merge pull request #37 from trivedigaurav/tts_example. [akshayaurora] + + Text to Speech Example + +- Removed an empty line. [gtrivedi] + +- Created an example application for the text to speech (tts) facade. + [gtrivedi] + +- Merge pull request #11 from kivy/notification_windows_icon. + [akshayaurora] + + User-specified icon support for Windows notifications + +- Notification: introduce `timeout` [qua-non] + +- User-specified icon support for Windows notifications. [Ben Rousch] + +- Merge pull request #15 from voen/patch-1. [Ben Rousch] + + readme typo corrected + +- Readme typo corrected. [voen] + +- Merge pull request #10 from kivy/dbus_notify. [akshayaurora] + + Introduce dbus notification + +- Removed attempts at using app_icon in Android notification. [Ben + Rousch] + +- Improve notification example. [qua-non] + +- Partial example notification example. [Ben Rousch] + +- Introduce dbus notification. [qua-non] + +- Fix plyer android.activity import. [Mathieu Virbel] + +- Fixed whereis_exe for windows. Fixed espeak TTS for windows. [Ben + Rousch] + +- Merge pull request #5 from inclement/sendemail. [Mathieu Virbel] + + Added an email facade and basic android implementation + +- Removed unnecessary import from email. [Alexander Taylor] + +- Pep8 fixes. [Alexander Taylor] + +- Added email facade example. [Alexander Taylor] + +- Added email to README. [Alexander Taylor] + +- Completed email facade and android implementation. [Alexander Taylor] + +- Added email facade. [Alexander Taylor] + +- Added simple buildozer.spec for gps example. [Alexander Taylor] + +- Add missing super() constructor in IosAccelerometer. [Mathieu Virbel] + +- Ios: add support for accelerometer on iOS (and motivate brousch again) + [Mathieu Virbel] + +- Add MANIFEST to include LICENSE and README. bump to 1.1.2. [Mathieu + Virbel] + +- Bump to 1.1.1. [Mathieu Virbel] + +- Fix setup for pip. [Mathieu Virbel] + +- Update readme. [Mathieu Virbel] + +- Setup.py: fix readme. [Mathieu Virbel] + +- Update readme. [Mathieu Virbel] + +- Gps: add versionadded. [Mathieu Virbel] + +- Fix documentation version. [Mathieu Virbel] + +- Gps: update documentation. [Mathieu Virbel] + +- Update setup.py to correctly include win. [Mathieu Virbel] + +- Merge branch 'master' of ssh://github.com/kivy/plyer. [Mathieu Virbel] + +- Add a basic grid to show supported features per os. [tshirtman] + +- Custom icon specification for android notification. [Ben Rousch] + +- Even more elegant Android notification code. [Ben Rousch] + +- Less hacky getPackageName from Android. [Ben Rousch] + +- Fixed hasattr bug. Added default icon support for Android + notifications. [Ben Rousch] + +- PEPed up balloontip. Fixed no 2nd notify bug. Fixed blocking + notification bug. [Ben Rousch] + +- Added notifications for Linux via notify-send. [Ben Rousch] + +- Fixed name of Windows platform, fixed Windows notify. [Ben Rousch] + +- Add GPS/android support for plyer. [Mathieu Virbel] + +- Add setup.py. [Mathieu Virbel] + +- Plyer is now under MIT license. [Mathieu Virbel] + +- Fixed incorrect Android tTS return type. [Ben Rousch] + +- Merge pull request #1 from kivy/tts. [Ben Rousch] + + TTS! + +- Changed NotImplemented exception to NotImplementedError. [Ben Rousch] + +- Added missing () to TTS returns. [Ben Rousch] + +- Fixed missed TextToSpeech, deleted old files. [Ben Rousch] + +- Changed TextToSpeech to TTS. Returning TTS instead of raising + NotImplemented. [Ben Rousch] + +- Added text_to_speech for Android, Linux, OSX, and Windows. [Ben + Rousch] + +- Ensure the documentation will find plyer. [Mathieu Virbel] + +- Rework how implementation works, and start documentation. [Mathieu + Virbel] + +- First version of plyer, including accelerometer (android), camera + (android) and notification (android, osx). api is not stabilized. + [Mathieu Virbel] + +- Merge branch 'master' of github.com:kivy/plyer. [tshirtman] + + Conflicts: + readme.md + +- Update readme.md. [Gabriel Pettier] + +- Rename to plyer, and uses plateform() from kivy utils. [tshirtman] + +- Add android/desktop/ios modules, and auto import from them. + [tshirtman] + +- Initial commit, created simple readme. [tshirtman] + + diff --git a/external/plyer/LICENSE b/external/plyer/LICENSE new file mode 100644 index 0000000..b336a3a --- /dev/null +++ b/external/plyer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2015 Kivy Team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/external/plyer/README.rst b/external/plyer/README.rst new file mode 100644 index 0000000..56860db --- /dev/null +++ b/external/plyer/README.rst @@ -0,0 +1,38 @@ +Plyer +===== + +Plyer is a platform-independent api to use features commonly found on various +platforms, notably mobile ones, in Python. + +How +--- + +Plyer tries not to reinvent the wheel, and will call for external libraries to +implement the api in the easiest way, depending on the current platform. + +- on python-for-android, pyjnius is used +- on kivy-ios, pyobjus is used +- on windows/mac/linux, commonly found libraries and programs will be used + +Support +------- + +================================== ============= ============= === ======= === ===== +Platform Android < 4.0 Android > 4.0 iOS Windows OSX Linux +================================== ============= ============= === ======= === ===== +Accelerometer X X X X X +Camera (taking picture) X X +GPS X X X +Notifications X X X X X +Text to speech X X X X X X +Email (open mail client) X X X X X X +Vibrator X X +Sms (send messages) X X +Compass X X X +Unique ID (IMEI or SN) X X X X X X +Gyroscope X X X +Battery X X X X X X +Native file chooser X X X +Orientation X X +Audio recording X X +================================== ============= ============= === ======= === ===== diff --git a/external/plyer/__init__.py b/external/plyer/__init__.py new file mode 100644 index 0000000..f8e35e5 --- /dev/null +++ b/external/plyer/__init__.py @@ -0,0 +1,63 @@ +''' +Plyer +===== + +''' + +__all__ = ('accelerometer', 'audio', 'battery', 'camera', 'compass', 'email', + 'filechooser', 'gps', 'gyroscope', 'irblaster', 'orientation', + 'notification', 'sms', 'tts', 'uniqueid', 'vibrator') + +__version__ = '1.2.4' + + +from plyer import facades +from plyer.utils import Proxy + +#: Accelerometer proxy to :class:`plyer.facades.Accelerometer` +accelerometer = Proxy('accelerometer', facades.Accelerometer) + +#: Audio proxy to :class:`plyer.facades.Audio` +audio = Proxy('audio', facades.Audio) + +#: Battery proxy to :class:`plyer.facades.Battery` +battery = Proxy('battery', facades.Battery) + +#: Compass proxy to :class:`plyer.facades.Compass` +compass = Proxy('compass', facades.Compass) + +#: Camera proxy to :class:`plyer.facades.Camera` +camera = Proxy('camera', facades.Camera) + +#: Email proxy to :class:`plyer.facades.Email` +email = Proxy('email', facades.Email) + +#: FileChooser proxy to :class:`plyer.facades.FileChooser` +filechooser = Proxy('filechooser', facades.FileChooser) + +#: GPS proxy to :class:`plyer.facades.GPS` +gps = Proxy('gps', facades.GPS) + +#: Gyroscope proxy to :class:`plyer.facades.Gyroscope` +gyroscope = Proxy('gyroscope', facades.Gyroscope) + +#: IrBlaster proxy to :class:`plyer.facades.IrBlaster` +irblaster = Proxy('irblaster', facades.IrBlaster) + +#: Orientation proxy to :class:`plyer.facades.Orientation` +orientation = Proxy('orientation', facades.Orientation) + +#: Notification proxy to :class:`plyer.facades.Notification` +notification = Proxy('notification', facades.Notification) + +#: Sms proxy to :class:`plyer.facades.Sms` +sms = Proxy('sms', facades.Sms) + +#: TTS proxy to :class:`plyer.facades.TTS` +tts = Proxy('tts', facades.TTS) + +#: UniqueID proxy to :class:`plyer.facades.UniqueID` +uniqueid = Proxy('uniqueid', facades.UniqueID) + +#: Vibrator proxy to :class:`plyer.facades.Vibrator` +vibrator = Proxy('vibrator', facades.Vibrator) diff --git a/external/plyer/__init__.pyc b/external/plyer/__init__.pyc Binary files differnew file mode 100644 index 0000000..d78a630 --- /dev/null +++ b/external/plyer/__init__.pyc diff --git a/external/plyer/compat.py b/external/plyer/compat.py new file mode 100644 index 0000000..fee64da --- /dev/null +++ b/external/plyer/compat.py @@ -0,0 +1,34 @@ +''' +Compatibility module for Python 2.7 and > 3.3 +============================================= +''' + +__all__ = ('PY2', 'string_types', 'queue', 'iterkeys', + 'itervalues', 'iteritems') + +import sys +try: + import queue +except ImportError: + import Queue as queue + +#: True if Python 2 intepreter is used +PY2 = sys.version_info[0] == 2 + +#: String types that can be used for checking if a object is a string +string_types = None +text_type = None +if PY2: + string_types = basestring + text_type = unicode +else: + string_types = text_type = str + +if PY2: + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() +else: + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) diff --git a/external/plyer/facades/__init__.py b/external/plyer/facades/__init__.py new file mode 100644 index 0000000..e5560c0 --- /dev/null +++ b/external/plyer/facades/__init__.py @@ -0,0 +1,28 @@ +''' +Facades +======= + +Interface of all the features available. + +''' + +__all__ = ('Accelerometer', 'Audio', 'Battery', 'Camera', 'Compass', 'Email', + 'FileChooser', 'GPS', 'Gyroscope', 'IrBlaster', 'Orientation', + 'Notification', 'Sms', 'TTS', 'UniqueID', 'Vibrator') + +from plyer.facades.accelerometer import Accelerometer +from plyer.facades.audio import Audio +from plyer.facades.battery import Battery +from plyer.facades.camera import Camera +from plyer.facades.compass import Compass +from plyer.facades.email import Email +from plyer.facades.filechooser import FileChooser +from plyer.facades.gps import GPS +from plyer.facades.gyroscope import Gyroscope +from plyer.facades.irblaster import IrBlaster +from plyer.facades.orientation import Orientation +from plyer.facades.notification import Notification +from plyer.facades.sms import Sms +from plyer.facades.tts import TTS +from plyer.facades.uniqueid import UniqueID +from plyer.facades.vibrator import Vibrator diff --git a/external/plyer/facades/__init__.pyc b/external/plyer/facades/__init__.pyc Binary files differnew file mode 100644 index 0000000..8058b7a --- /dev/null +++ b/external/plyer/facades/__init__.pyc diff --git a/external/plyer/facades/accelerometer.py b/external/plyer/facades/accelerometer.py new file mode 100644 index 0000000..e8146bf --- /dev/null +++ b/external/plyer/facades/accelerometer.py @@ -0,0 +1,36 @@ +class Accelerometer(object): + '''Accelerometer facade. + ''' + + @property + def acceleration(self): + '''Property that returns values of the current acceleration + sensors, as a (x, y, z) tuple. Returns (None, None, None) + if no data is currently available. + ''' + return self.get_acceleration() + + def enable(self): + '''Activate the accelerometer sensor. Throws an error if the + hardware is not available or not implemented on. + ''' + self._enable() + + def disable(self): + '''Disable the accelerometer sensor. + ''' + self._disable() + + def get_acceleration(self): + return self._get_acceleration() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_acceleration(self): + raise NotImplementedError() diff --git a/external/plyer/facades/accelerometer.pyc b/external/plyer/facades/accelerometer.pyc Binary files differnew file mode 100644 index 0000000..c646a98 --- /dev/null +++ b/external/plyer/facades/accelerometer.pyc diff --git a/external/plyer/facades/audio.py b/external/plyer/facades/audio.py new file mode 100644 index 0000000..5678841 --- /dev/null +++ b/external/plyer/facades/audio.py @@ -0,0 +1,56 @@ +class Audio(object): + '''Audio Facade. + + Used for recording audio. + Use method `start` to start record and `stop` for stop recording. + To hear what you have just recorded, use method `play`. + + `status` will tell you about current job of the Audio. + + Default path for recording is set in platform implementation. + + .. note:: + On Android the `RECORD_AUDIO` permission is needed. + ''' + + state = 'ready' + _file_path = '' + + def __init__(self, file_path): + super(Audio, self).__init__() + self._file_path = file_path + + def start(self): + '''Start record.''' + self._start() + self.state = 'recording' + + def _start(self): + raise NotImplementedError() + + def stop(self): + '''Stop record.''' + self._stop() + self.state = 'ready' + + def _stop(self): + raise NotImplementedError() + + def play(self): + '''Play current recording.''' + self._play() + self.state = 'playing' + + def _play(self): + raise NotImplementedError() + + @property + def file_path(self): + return self._file_path + + @file_path.setter + def file_path(self, location): + '''Location of the recording.''' + assert isinstance(location, (basestring, unicode)), \ + 'Location must be string or unicode' + self._file_path = location diff --git a/external/plyer/facades/audio.pyc b/external/plyer/facades/audio.pyc Binary files differnew file mode 100644 index 0000000..9b79fcf --- /dev/null +++ b/external/plyer/facades/audio.pyc diff --git a/external/plyer/facades/battery.py b/external/plyer/facades/battery.py new file mode 100644 index 0000000..7cd48c7 --- /dev/null +++ b/external/plyer/facades/battery.py @@ -0,0 +1,22 @@ +class Battery(object): + '''Battery info facade.''' + + @property + def status(self): + '''Property that contains a dict with the following fields: + * **isCharging** *(bool)*: Battery is charging + * **percentage** *(float)*: Battery charge remaining + + .. warning:: + If any of the fields is not readable, it is set as + None. + ''' + return self.get_state() + + def get_state(self): + return self._get_state() + + #private + + def _get_state(self): + raise NotImplementedError() diff --git a/external/plyer/facades/battery.pyc b/external/plyer/facades/battery.pyc Binary files differnew file mode 100644 index 0000000..07f25d7 --- /dev/null +++ b/external/plyer/facades/battery.pyc diff --git a/external/plyer/facades/camera.py b/external/plyer/facades/camera.py new file mode 100644 index 0000000..5300ede --- /dev/null +++ b/external/plyer/facades/camera.py @@ -0,0 +1,43 @@ +class Camera(object): + '''Camera facade. + ''' + + def take_picture(self, filename, on_complete): + '''Ask the OS to capture a picture, and store it at filename. + + When the capture is done, on_complete will be called with the filename + as an argument. If the callback returns True, the filename will be + unlinked. + + :param filename: Name of the image file + :param on_complete: Callback that will be called when the operation is + done + + :type filename: str + :type on_complete: callable + ''' + self._take_picture(filename=filename, on_complete=on_complete) + + def take_video(self, filename, on_complete): + '''Ask the OS to capture a video, and store it at filename. + + When the capture is done, on_complete will be called with the filename + as an argument. If the callback returns True, the filename will be + unlinked. + + :param filename: Name of the video file + :param on_complete: Callback that will be called when the operation is + done + + :type filename: str + :type on_complete: callable + ''' + self._take_video(filename=filename, on_complete=on_complete) + + # private + + def _take_picture(self, **kwargs): + raise NotImplementedError() + + def _take_video(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/camera.pyc b/external/plyer/facades/camera.pyc Binary files differnew file mode 100644 index 0000000..5146deb --- /dev/null +++ b/external/plyer/facades/camera.pyc diff --git a/external/plyer/facades/compass.py b/external/plyer/facades/compass.py new file mode 100644 index 0000000..aed4bbf --- /dev/null +++ b/external/plyer/facades/compass.py @@ -0,0 +1,37 @@ +class Compass(object): + '''Compass facade. + + .. versionadded:: 1.2.0 + ''' + + @property + def orientation(self): + '''Property that returns values of the current compass + (magnetic field) sensors, as a (x, y, z) tuple. + Returns (None, None, None) if no data is currently available. + ''' + return self.get_orientation() + + def enable(self): + '''Activate the compass sensor. + ''' + self._enable() + + def disable(self): + '''Disable the compass sensor. + ''' + self._disable() + + def get_orientation(self): + return self._get_orientation() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_orientation(self): + raise NotImplementedError() diff --git a/external/plyer/facades/compass.pyc b/external/plyer/facades/compass.pyc Binary files differnew file mode 100644 index 0000000..af2e244 --- /dev/null +++ b/external/plyer/facades/compass.pyc diff --git a/external/plyer/facades/email.py b/external/plyer/facades/email.py new file mode 100644 index 0000000..6056a16 --- /dev/null +++ b/external/plyer/facades/email.py @@ -0,0 +1,23 @@ +class Email(object): + '''Email facade.''' + + def send(self, recipient=None, subject=None, text=None, + create_chooser=None): + '''Open an email client message send window, prepopulated with the + given arguments. + + :param recipient: Recipient of the message (str) + :param subject: Subject of the message (str) + :param text: Main body of the message (str) + :param create_chooser: Whether to display a program chooser to + handle the message (bool) + + .. note:: create_chooser is only supported on Android + ''' + self._send(recipient=recipient, subject=subject, text=text, + create_chooser=create_chooser) + + # private + + def _send(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/email.pyc b/external/plyer/facades/email.pyc Binary files differnew file mode 100644 index 0000000..b1ee2bf --- /dev/null +++ b/external/plyer/facades/email.pyc diff --git a/external/plyer/facades/filechooser.py b/external/plyer/facades/filechooser.py new file mode 100644 index 0000000..040e9bb --- /dev/null +++ b/external/plyer/facades/filechooser.py @@ -0,0 +1,53 @@ +class FileChooser(object): + '''Native filechooser dialog facade. + + open_file, save_file and choose_dir accept a number of arguments + listed below. They return either a list of paths (normally + absolute), or None if no file was selected or the operation was + canceled and no result is available. + + Arguments: + * **path** *(string or None)*: a path that will be selected + by default, or None + * **multiple** *(bool)*: True if you want the dialog to + allow multiple file selection. (Note: Windows doesn't + support multiple directory selection) + * **filters** *(iterable)*: either a list of wildcard patterns + or of sequences that contain the name of the filter and any + number of wildcards that will be grouped under that name + (e.g. [["Music", "*mp3", "*ogg", "*aac"], "*jpg", "*py"]) + * **preview** *(bool)*: True if you want the file chooser to + show a preview of the selected file, if supported by the + back-end. + * **title** *(string or None)*: The title of the file chooser + window, or None for the default title. + * **icon** *(string or None)*: Path to the icon of the file + chooser window (where supported), or None for the back-end's + default. + * **show_hidden** *(bool)*: Force showing hidden files (currently + supported only on Windows) + + Important: these methods will return only after user interaction. + Use threads or you will stop the mainloop if your app has one. + ''' + + def _file_selection_dialog(self, **kwargs): + raise NotImplementedError() + + def open_file(self, *args, **kwargs): + """Open the file chooser in "open" mode. + """ + return self._file_selection_dialog(mode="open", *args, **kwargs) + + def save_file(self, *args, **kwargs): + """Open the file chooser in "save" mode. Confirmation will be asked + when a file with the same name already exists. + """ + return self._file_selection_dialog(mode="save", *args, **kwargs) + + def choose_dir(self, *args, **kwargs): + """Open the directory chooser. Note that on Windows this is very + limited. Consider writing your own chooser if you target that + platform and are planning on using unsupported features. + """ + return self._file_selection_dialog(mode="dir", *args, **kwargs) diff --git a/external/plyer/facades/filechooser.pyc b/external/plyer/facades/filechooser.pyc Binary files differnew file mode 100644 index 0000000..70c0f61 --- /dev/null +++ b/external/plyer/facades/filechooser.pyc diff --git a/external/plyer/facades/gps.py b/external/plyer/facades/gps.py new file mode 100644 index 0000000..09f7e4b --- /dev/null +++ b/external/plyer/facades/gps.py @@ -0,0 +1,67 @@ +class GPS(object): + '''GPS facade. + + .. versionadded:: 1.1 + + You need to set a `on_location` callback with the :meth:`configure` method. + This callback will receive a couple of keywords / values, that might be + different depending of their availability on the targeted platform. + Lat and lon are always available. + + - lat: latitude of the last location, in degrees + - lon: longitude of the last location, in degrees + - speed: speed of the user, in meters/second over ground + - bearing: bearing in degrees + - altitude: altitude in meters above the sea level + + Here is an example of the usage of gps:: + + from plyer import gps + + def print_locations(**kwargs): + print 'lat: {lat}, lon: {lon}'.format(**kwargs) + + gps.configure(on_location=print_locations) + gps.start() + # later + gps.stop() + ''' + + def configure(self, on_location, on_status=None): + '''Configure the GPS object. This method should be called before + :meth:`start`. + + :param on_location: Function to call when receiving a new location + :param on_status: Function to call when a status message is received + :type on_location: callable, multiples keys/value will be passed. + :type on_status: callable, args are "message-type", "status" + + .. warning:: + + The `on_location` and `on_status` callables might be called from + another thread than the thread used for creating the GPS object. + ''' + self.on_location = on_location + self.on_status = on_status + self._configure() + + def start(self): + '''Start the GPS location updates. + ''' + self._start() + + def stop(self): + '''Stop the GPS location updates. + ''' + self._stop() + + # private + + def _configure(self): + raise NotImplementedError() + + def _start(self): + raise NotImplementedError() + + def _stop(self): + raise NotImplementedError() diff --git a/external/plyer/facades/gps.pyc b/external/plyer/facades/gps.pyc Binary files differnew file mode 100644 index 0000000..175cf9e --- /dev/null +++ b/external/plyer/facades/gps.pyc diff --git a/external/plyer/facades/gyroscope.py b/external/plyer/facades/gyroscope.py new file mode 100644 index 0000000..31ec1ae --- /dev/null +++ b/external/plyer/facades/gyroscope.py @@ -0,0 +1,37 @@ +class Gyroscope(object): + '''Gyroscope facade. + + .. versionadded:: 1.2.0 + ''' + + @property + def orientation(self): + '''Property that returns values of the current Gyroscope sensors, as + a (x, y, z) tuple. Returns (None, None, None) if no data is currently + available. + ''' + return self.get_orientation() + + def enable(self): + '''Activate the Gyroscope sensor. + ''' + self._enable() + + def disable(self): + '''Disable the Gyroscope sensor. + ''' + self._disable() + + def get_orientation(self): + return self._get_orientation() + + # private + + def _enable(self): + raise NotImplementedError() + + def _disable(self): + raise NotImplementedError() + + def _get_orientation(self): + raise NotImplementedError() diff --git a/external/plyer/facades/gyroscope.pyc b/external/plyer/facades/gyroscope.pyc Binary files differnew file mode 100644 index 0000000..c6989c6 --- /dev/null +++ b/external/plyer/facades/gyroscope.pyc diff --git a/external/plyer/facades/irblaster.py b/external/plyer/facades/irblaster.py new file mode 100644 index 0000000..0e15a07 --- /dev/null +++ b/external/plyer/facades/irblaster.py @@ -0,0 +1,59 @@ +class IrBlaster(object): + '''Infrared blaster facade.''' + + @staticmethod + def periods_to_microseconds(frequency, pattern): + '''Convert a pattern from period counts to microseconds. + ''' + period = 1000000. / frequency + return [period * x for x in pattern] + + @staticmethod + def microseconds_to_periods(frequency, pattern): + '''Convert a pattern from microseconds to period counts. + ''' + period = 1000000. / frequency + return [x / period for x in pattern] + + @property + def frequencies(self): + '''Property which contains a list of frequency ranges + supported by the device in the form: + + [(from1, to1), + (from2, to2), + ... + (fromN, toN)] + ''' + return self.get_frequencies() + + def get_frequencies(self): + return self._get_frequencies() + + def _get_frequencies(self): + raise NotImplementedError() + + def transmit(self, frequency, pattern, mode='period'): + '''Transmit an IR sequence. + + :parameters: + `frequency`: int + Carrier frequency for the IR transmission. + `pattern`: list[int] + Burst pair pattern to transmit. + `mode`: str, defaults to 'period' + Specifies the format of the pattern values. + Can be 'period' or 'microseconds'. + ''' + return self._transmit(frequency, pattern, mode) + + def _transmit(self, frequency, pattern, mode): + raise NotImplementedError() + + def exists(self): + '''Check if the device has an infrared emitter. + ''' + return self._exists() + + def _exists(self): + raise NotImplementedError() diff --git a/external/plyer/facades/irblaster.pyc b/external/plyer/facades/irblaster.pyc Binary files differnew file mode 100644 index 0000000..74c9d71 --- /dev/null +++ b/external/plyer/facades/irblaster.pyc diff --git a/external/plyer/facades/notification.py b/external/plyer/facades/notification.py new file mode 100644 index 0000000..3a477e9 --- /dev/null +++ b/external/plyer/facades/notification.py @@ -0,0 +1,26 @@ +class Notification(object): + '''Notification facade. + ''' + + def notify(self, title='', message='', app_name='', app_icon='', + timeout=10): + '''Send a notification. + + :param title: Title of the notification + :param message: Message of the notification + :param app_name: Name of the app launching this notification + :param app_icon: Icon to be displayed along with the message + :param timeout: time to display the message for, defaults to 10 + :type title: str + :type message: str + :type app_name: str + :type app_icon: str + :type timeout: int + ''' + self._notify(title=title, message=message, app_icon=app_icon, + app_name=app_name, timeout=timeout) + + # private + + def _notify(self, **kwargs): + raise NotImplementedError("No usable implementation found!") diff --git a/external/plyer/facades/notification.pyc b/external/plyer/facades/notification.pyc Binary files differnew file mode 100644 index 0000000..8a31ae3 --- /dev/null +++ b/external/plyer/facades/notification.pyc diff --git a/external/plyer/facades/orientation.py b/external/plyer/facades/orientation.py new file mode 100644 index 0000000..e58e723 --- /dev/null +++ b/external/plyer/facades/orientation.py @@ -0,0 +1,45 @@ +class Orientation(object): + '''Orientation facade. + + .. note:: + These settings are generally guidelines, the operating + system may choose to ignore them, or they may be overridden by + other system components. + + .. versionadded:: 1.2.4 + ''' + + def set_landscape(self, reverse=False): + '''Rotate the app to a landscape orientation. + + :param reverse: If True, uses the opposite of the natural + orientation. + ''' + self._set_landscape(reverse=reverse) + + def _set_landscape(self, **kwargs): + raise NotImplementedError() + + def set_portrait(self, reverse=False): + '''Rotate the app to a portrait orientation. + + :param reverse: If True, uses the opposite of the natural + orientation. + ''' + self._set_portrait(reverse=reverse) + + def _set_portrait(self, **kwargs): + raise NotImplementedError() + + def set_sensor(self, mode='any'): + '''Rotate freely following sensor information from the device. + + :param mode: The rotation mode, should be one of 'any' (rotate + to any orientation), 'landscape' (choose nearest + landscape mode) or 'portrait' (choose nearest + portrait mode). Defaults to 'any'. + ''' + self._set_sensor(mode=mode) + + def _set_sensor(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/orientation.pyc b/external/plyer/facades/orientation.pyc Binary files differnew file mode 100644 index 0000000..23deae8 --- /dev/null +++ b/external/plyer/facades/orientation.pyc diff --git a/external/plyer/facades/sms.py b/external/plyer/facades/sms.py new file mode 100644 index 0000000..ce4f299 --- /dev/null +++ b/external/plyer/facades/sms.py @@ -0,0 +1,19 @@ +class Sms(object): + '''Sms facade. + + .. note:: + + On Android your app needs the SEND_SMS permission in order to + send sms messages. + + .. versionadded:: 1.2.0 + + ''' + + def send(self, recipient, message): + self._send(recipient=recipient, message=message) + + # private + + def _send(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/sms.pyc b/external/plyer/facades/sms.pyc Binary files differnew file mode 100644 index 0000000..7294345 --- /dev/null +++ b/external/plyer/facades/sms.pyc diff --git a/external/plyer/facades/tts.py b/external/plyer/facades/tts.py new file mode 100644 index 0000000..91c1a76 --- /dev/null +++ b/external/plyer/facades/tts.py @@ -0,0 +1,16 @@ +class TTS(object): + '''TextToSpeech facade. + ''' + + def speak(self, message=''): + '''Use text to speech capabilities to speak the message. + + :param message: What to speak + :type message: str + ''' + self._speak(message=message) + + # private + + def _speak(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/tts.pyc b/external/plyer/facades/tts.pyc Binary files differnew file mode 100644 index 0000000..d5d395f --- /dev/null +++ b/external/plyer/facades/tts.pyc diff --git a/external/plyer/facades/uniqueid.py b/external/plyer/facades/uniqueid.py new file mode 100644 index 0000000..d916f01 --- /dev/null +++ b/external/plyer/facades/uniqueid.py @@ -0,0 +1,29 @@ +class UniqueID(object): + '''UniqueID facade. + + Returns the following depending on the platform: + + * **Android**: Android ID + * **OS X**: Serial number of the device + * **Linux**: Serial number using lshw + * **Windows**: MachineGUID from regkey + + .. versionadded:: 1.2.0 + + .. versionchanged:: 1.2.4 + On Android returns Android ID instead of IMEI. + ''' + + @property + def id(self): + '''Property that returns the unique id of the platform. + ''' + return self.get_uid() + + def get_uid(self): + return self._get_uid() + + # private + + def _get_uid(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/uniqueid.pyc b/external/plyer/facades/uniqueid.pyc Binary files differnew file mode 100644 index 0000000..2d4c2ff --- /dev/null +++ b/external/plyer/facades/uniqueid.pyc diff --git a/external/plyer/facades/vibrator.py b/external/plyer/facades/vibrator.py new file mode 100644 index 0000000..94fe9aa --- /dev/null +++ b/external/plyer/facades/vibrator.py @@ -0,0 +1,53 @@ +class Vibrator(object): + '''Vibration facade. + + .. note:: + On Android your app needs the VIBRATE permission to + access the vibrator. + ''' + + def vibrate(self, time=1): + '''Ask the vibrator to vibrate for the given period. + + :param time: Time to vibrate for, in seconds. Default is 1. + ''' + self._vibrate(time=time) + + def _vibrate(self, **kwargs): + raise NotImplementedError() + + def pattern(self, pattern=(0, 1), repeat=-1): + '''Ask the vibrator to vibrate with the given pattern, with an + optional repeat. + + :param pattern: Pattern to vibrate with. Should be a list of + times in seconds. The first number is how long to wait + before vibrating, and subsequent numbers are times to + vibrate and not vibrate alternately. + Defaults to ``[0, 1]``. + + :param repeat: Index at which to repeat the pattern. When the + vibration pattern reaches this index, it will start again + from the beginning. Defaults to ``-1``, which means no + repeat. + ''' + self._pattern(pattern=pattern, repeat=repeat) + + def _pattern(self, **kwargs): + raise NotImplementedError() + + def exists(self): + '''Check if the device has a vibrator. Returns True or + False. + ''' + return self._exists() + + def _exists(self, **kwargs): + raise NotImplementedError() + + def cancel(self): + '''Cancels any current vibration, and stops the vibrator.''' + self._cancel() + + def _cancel(self, **kwargs): + raise NotImplementedError() diff --git a/external/plyer/facades/vibrator.pyc b/external/plyer/facades/vibrator.pyc Binary files differnew file mode 100644 index 0000000..cec7d7f --- /dev/null +++ b/external/plyer/facades/vibrator.pyc diff --git a/external/plyer/platforms/__init__.py b/external/plyer/platforms/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/__init__.py diff --git a/external/plyer/platforms/android/__init__.py b/external/plyer/platforms/android/__init__.py new file mode 100644 index 0000000..6b565a3 --- /dev/null +++ b/external/plyer/platforms/android/__init__.py @@ -0,0 +1,12 @@ +from os import environ +from jnius import autoclass + +ANDROID_VERSION = autoclass('android.os.Build$VERSION') +SDK_INT = ANDROID_VERSION.SDK_INT + +if 'PYTHON_SERVICE_ARGUMENT' in environ: + PythonService = autoclass('org.renpy.android.PythonService') + activity = PythonService.mService +else: + PythonActivity = autoclass('org.renpy.android.PythonActivity') + activity = PythonActivity.mActivity diff --git a/external/plyer/platforms/android/accelerometer.py b/external/plyer/platforms/android/accelerometer.py new file mode 100644 index 0000000..af07c52 --- /dev/null +++ b/external/plyer/platforms/android/accelerometer.py @@ -0,0 +1,74 @@ +''' +Android accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class AccelerometerSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super(AccelerometerSensorListener, self).__init__() + self.SensorManager = cast('android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE)) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_ACCELEROMETER) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidAccelerometer(Accelerometer): + def __init__(self): + super(AndroidAccelerometer, self).__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = AccelerometerSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_acceleration(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super(self.__class__, self).__del__() + + +def instance(): + return AndroidAccelerometer() diff --git a/external/plyer/platforms/android/audio.py b/external/plyer/platforms/android/audio.py new file mode 100644 index 0000000..2115f19 --- /dev/null +++ b/external/plyer/platforms/android/audio.py @@ -0,0 +1,58 @@ +from jnius import autoclass + +from plyer.facades.audio import Audio + +# Recorder Classes +MediaRecorder = autoclass('android.media.MediaRecorder') +AudioSource = autoclass('android.media.MediaRecorder$AudioSource') +OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat') +AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder') + +# Player Classes +MediaPlayer = autoclass('android.media.MediaPlayer') + + +class AndroidAudio(Audio): + '''Audio for android. + + For recording audio we use MediaRecorder Android class. + For playing audio we use MediaPlayer Android class. + ''' + + def __init__(self, file_path=None): + default_path = '/sdcard/testrecorder.3gp' + super(AndroidAudio, self).__init__(file_path or default_path) + + self._recorder = None + self._player = None + + def _start(self): + self._recorder = MediaRecorder() + self._recorder.setAudioSource(AudioSource.DEFAULT) + self._recorder.setOutputFormat(OutputFormat.DEFAULT) + self._recorder.setAudioEncoder(AudioEncoder.DEFAULT) + self._recorder.setOutputFile(self.file_path) + + self._recorder.prepare() + self._recorder.start() + + def _stop(self): + if self._recorder: + self._recorder.stop() + self._recorder.release() + self._recorder = None + + if self._player: + self._player.stop() + self._player.release() + self._player = None + + def _play(self): + self._player = MediaPlayer() + self._player.setDataSource(self.file_path) + self._player.prepare() + self._player.start() + + +def instance(): + return AndroidAudio() diff --git a/external/plyer/platforms/android/battery.py b/external/plyer/platforms/android/battery.py new file mode 100644 index 0000000..2ade1d2 --- /dev/null +++ b/external/plyer/platforms/android/battery.py @@ -0,0 +1,34 @@ +from jnius import autoclass, cast +from plyer.platforms.android import activity +from plyer.facades import Battery + +Intent = autoclass('android.content.Intent') +BatteryManager = autoclass('android.os.BatteryManager') +IntentFilter = autoclass('android.content.IntentFilter') + + +class AndroidBattery(Battery): + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + + batteryStatus = cast('android.content.Intent', + activity.registerReceiver(None, ifilter)) + + query = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + isCharging = (query == BatteryManager.BATTERY_STATUS_CHARGING or + query == BatteryManager.BATTERY_STATUS_FULL) + + level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + percentage = level / float(scale) + + status['isCharging'] = isCharging + status['percentage'] = percentage + + return status + + +def instance(): + return AndroidBattery() diff --git a/external/plyer/platforms/android/camera.py b/external/plyer/platforms/android/camera.py new file mode 100644 index 0000000..344296d --- /dev/null +++ b/external/plyer/platforms/android/camera.py @@ -0,0 +1,59 @@ +import android +import android.activity +from os import unlink +from jnius import autoclass, cast +from plyer.facades import Camera +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +PythonActivity = autoclass('org.renpy.android.PythonActivity') +MediaStore = autoclass('android.provider.MediaStore') +Uri = autoclass('android.net.Uri') + + +class AndroidCamera(Camera): + + def _take_picture(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + activity.startActivityForResult(intent, 0x123) + + def _take_video(self, on_complete, filename=None): + assert(on_complete is not None) + self.on_complete = on_complete + self.filename = filename + android.activity.unbind(on_activity_result=self._on_activity_result) + android.activity.bind(on_activity_result=self._on_activity_result) + intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + uri = Uri.parse('file://' + filename) + parcelable = cast('android.os.Parcelable', uri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable) + + # 0 = low quality, suitable for MMS messages, + # 1 = high quality + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1) + activity.startActivityForResult(intent, 0x123) + + def _on_activity_result(self, requestCode, resultCode, intent): + if requestCode != 0x123: + return + android.activity.unbind(on_activity_result=self._on_activity_result) + if self.on_complete(self.filename): + self._unlink(self.filename) + + def _unlink(self, fn): + try: + unlink(fn) + except: + pass + + +def instance(): + return AndroidCamera() diff --git a/external/plyer/platforms/android/compass.py b/external/plyer/platforms/android/compass.py new file mode 100644 index 0000000..7fb19d6 --- /dev/null +++ b/external/plyer/platforms/android/compass.py @@ -0,0 +1,74 @@ +''' +Android Compass +--------------------- +''' + +from plyer.facades import Compass +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class MagneticFieldSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super(MagneticFieldSensorListener, self).__init__() + self.SensorManager = cast('android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE)) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_MAGNETIC_FIELD) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidCompass(Compass): + def __init__(self): + super(AndroidCompass, self).__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = MagneticFieldSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_orientation(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super(self.__class__, self).__del__() + + +def instance(): + return AndroidCompass() diff --git a/external/plyer/platforms/android/email.py b/external/plyer/platforms/android/email.py new file mode 100644 index 0000000..79923e4 --- /dev/null +++ b/external/plyer/platforms/android/email.py @@ -0,0 +1,40 @@ +from jnius import autoclass, cast +from plyer.facades import Email +from plyer.platforms.android import activity + +Intent = autoclass('android.content.Intent') +AndroidString = autoclass('java.lang.String') + + +class AndroidEmail(Email): + def _send(self, **kwargs): + intent = Intent(Intent.ACTION_SEND) + intent.setType('text/plain') + + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + create_chooser = kwargs.get('create_chooser') + + if recipient: + intent.putExtra(Intent.EXTRA_EMAIL, [recipient]) + if subject: + android_subject = cast('java.lang.CharSequence', + AndroidString(subject)) + intent.putExtra(Intent.EXTRA_SUBJECT, android_subject) + if text: + android_text = cast('java.lang.CharSequence', + AndroidString(text)) + intent.putExtra(Intent.EXTRA_TEXT, android_text) + + if create_chooser: + chooser_title = cast('java.lang.CharSequence', + AndroidString('Send message with:')) + activity.startActivity(Intent.createChooser(intent, + chooser_title)) + else: + activity.startActivity(intent) + + +def instance(): + return AndroidEmail() diff --git a/external/plyer/platforms/android/gps.py b/external/plyer/platforms/android/gps.py new file mode 100644 index 0000000..fbe580f --- /dev/null +++ b/external/plyer/platforms/android/gps.py @@ -0,0 +1,79 @@ +''' +Android GPS +----------- +''' + +from plyer.facades import GPS +from plyer.platforms.android import activity +from jnius import autoclass, java_method, PythonJavaClass + +Looper = autoclass('android.os.Looper') +LocationManager = autoclass('android.location.LocationManager') +Context = autoclass('android.content.Context') + + +class _LocationListener(PythonJavaClass): + __javainterfaces__ = ['android/location/LocationListener'] + + def __init__(self, root): + self.root = root + super(_LocationListener, self).__init__() + + @java_method('(Landroid/location/Location;)V') + def onLocationChanged(self, location): + self.root.on_location( + lat=location.getLatitude(), + lon=location.getLongitude(), + speed=location.getSpeed(), + bearing=location.getBearing(), + altitude=location.getAltitude()) + + @java_method('(Ljava/lang/String;)V') + def onProviderEnabled(self, status): + if self.root.on_status: + self.root.on_status('provider-enabled', status) + + @java_method('(Ljava/lang/String;)V') + def onProviderDisabled(self, status): + if self.root.on_status: + self.root.on_status('provider-disabled', status) + + @java_method('(Ljava/lang/String;ILandroid/os/Bundle;)V') + def onStatusChanged(self, provider, status, extras): + if self.root.on_status: + s_status = 'unknown' + if status == 0x00: + s_status = 'out-of-service' + elif status == 0x01: + s_status = 'temporarily-unavailable' + elif status == 0x02: + s_status = 'available' + self.root.on_status('provider-status', '{}: {}'.format( + provider, s_status)) + + +class AndroidGPS(GPS): + + def _configure(self): + if not hasattr(self, '_location_manager'): + self._location_manager = activity.getSystemService( + Context.LOCATION_SERVICE) + self._location_listener = _LocationListener(self) + + def _start(self): + # XXX defaults should be configurable by the user, later + providers = self._location_manager.getProviders(False).toArray() + for provider in providers: + self._location_manager.requestLocationUpdates( + provider, + 1000, # minTime, in milliseconds + 1, # minDistance, in meters + self._location_listener, + Looper.getMainLooper()) + + def _stop(self): + self._location_manager.removeUpdates(self._location_listener) + + +def instance(): + return AndroidGPS() diff --git a/external/plyer/platforms/android/gyroscope.py b/external/plyer/platforms/android/gyroscope.py new file mode 100644 index 0000000..58747d7 --- /dev/null +++ b/external/plyer/platforms/android/gyroscope.py @@ -0,0 +1,74 @@ +''' +Android Gyroscope +--------------------- +''' + +from plyer.facades import Gyroscope +from jnius import PythonJavaClass, java_method, autoclass, cast +from plyer.platforms.android import activity + +Context = autoclass('android.content.Context') +Sensor = autoclass('android.hardware.Sensor') +SensorManager = autoclass('android.hardware.SensorManager') + + +class GyroscopeSensorListener(PythonJavaClass): + __javainterfaces__ = ['android/hardware/SensorEventListener'] + + def __init__(self): + super(GyroscopeSensorListener, self).__init__() + self.SensorManager = cast('android.hardware.SensorManager', + activity.getSystemService(Context.SENSOR_SERVICE)) + self.sensor = self.SensorManager.getDefaultSensor( + Sensor.TYPE_GYROSCOPE) + + self.values = [None, None, None] + + def enable(self): + self.SensorManager.registerListener(self, self.sensor, + SensorManager.SENSOR_DELAY_NORMAL) + + def disable(self): + self.SensorManager.unregisterListener(self, self.sensor) + + @java_method('(Landroid/hardware/SensorEvent;)V') + def onSensorChanged(self, event): + self.values = event.values[:3] + + @java_method('(Landroid/hardware/Sensor;I)V') + def onAccuracyChanged(self, sensor, accuracy): + # Maybe, do something in future? + pass + + +class AndroidGyroscope(Gyroscope): + def __init__(self): + super(AndroidGyroscope, self).__init__() + self.bState = False + + def _enable(self): + if (not self.bState): + self.listener = GyroscopeSensorListener() + self.listener.enable() + self.bState = True + + def _disable(self): + if (self.bState): + self.bState = False + self.listener.disable() + del self.listener + + def _get_orientation(self): + if (self.bState): + return tuple(self.listener.values) + else: + return (None, None, None) + + def __del__(self): + if(self.bState): + self._disable() + super(self.__class__, self).__del__() + + +def instance(): + return AndroidGyroscope() diff --git a/external/plyer/platforms/android/irblaster.py b/external/plyer/platforms/android/irblaster.py new file mode 100644 index 0000000..6c44717 --- /dev/null +++ b/external/plyer/platforms/android/irblaster.py @@ -0,0 +1,54 @@ +from jnius import autoclass + +from plyer.facades import IrBlaster +from plyer.platforms.android import activity, SDK_INT, ANDROID_VERSION + +if SDK_INT >= 19: + Context = autoclass('android.content.Context') + ir_manager = activity.getSystemService(Context.CONSUMER_IR_SERVICE) +else: + ir_manager = None + + +class AndroidIrBlaster(IrBlaster): + def _exists(self): + if ir_manager and ir_manager.hasIrEmitter(): + return True + return False + + @property + def multiply_pulse(self): + '''Android 4.4.3+ uses microseconds instead of period counts + ''' + return not (SDK_INT == 19 and + int(str(ANDROID_VERSION.RELEASE).rsplit('.', 1)[-1]) < 3) + + def _get_frequencies(self): + if not ir_manager: + return None + + if hasattr(self, '_frequencies'): + return self._frequencies + + ir_frequencies = ir_manager.getCarrierFrequencies() + if not ir_frequencies: + return [] + + frequencies = [] + for freqrange in ir_frequencies: + freq = (freqrange.getMinFrequency(), freqrange.getMaxFrequency()) + frequencies.append(freq) + + self._frequencies = frequencies + return frequencies + + def _transmit(self, frequency, pattern, mode): + if self.multiply_pulse and mode == 'period': + pattern = self.periods_to_microseconds(frequency, pattern) + elif not self.multiply_pulse and mode == 'microseconds': + pattern = self.microseconds_to_periods(frequency, pattern) + ir_manager.transmit(frequency, pattern) + + +def instance(): + return AndroidIrBlaster() diff --git a/external/plyer/platforms/android/notification.py b/external/plyer/platforms/android/notification.py new file mode 100644 index 0000000..bfc3a25 --- /dev/null +++ b/external/plyer/platforms/android/notification.py @@ -0,0 +1,37 @@ +from jnius import autoclass +from plyer.facades import Notification +from plyer.platforms.android import activity, SDK_INT + +AndroidString = autoclass('java.lang.String') +Context = autoclass('android.content.Context') +NotificationBuilder = autoclass('android.app.Notification$Builder') +Drawable = autoclass("{}.R$drawable".format(activity.getPackageName())) + + +class AndroidNotification(Notification): + def _get_notification_service(self): + if not hasattr(self, '_ns'): + self._ns = activity.getSystemService(Context.NOTIFICATION_SERVICE) + return self._ns + + def _notify(self, **kwargs): + icon = getattr(Drawable, kwargs.get('icon_android', 'icon')) + noti = NotificationBuilder(activity) + noti.setContentTitle(AndroidString( + kwargs.get('title').encode('utf-8'))) + noti.setContentText(AndroidString( + kwargs.get('message').encode('utf-8'))) + noti.setSmallIcon(icon) + noti.setAutoCancel(True) + + if SDK_INT >= 16: + noti = noti.build() + else: + noti = noti.getNotification() + + self._get_notification_service().notify(0, noti) + + +def instance(): + return AndroidNotification() + diff --git a/external/plyer/platforms/android/orientation.py b/external/plyer/platforms/android/orientation.py new file mode 100644 index 0000000..e98e34d --- /dev/null +++ b/external/plyer/platforms/android/orientation.py @@ -0,0 +1,43 @@ +from jnius import autoclass, cast +from plyer.platforms.android import activity +from plyer.facades import Orientation + +ActivityInfo = autoclass('android.content.pm.ActivityInfo') + + +class AndroidOrientation(Orientation): + + def _set_landscape(self, **kwargs): + reverse = kwargs.get('reverse') + if reverse: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + + def _set_portrait(self, **kwargs): + reverse = kwargs.get('reverse') + if reverse: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT) + else: + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) + + def _set_sensor(self, **kwargs): + mode = kwargs.get('mode') + + if mode == 'any': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR) + elif mode == 'landscape': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) + elif mode == 'portrait': + activity.setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) + + +def instance(): + return AndroidOrientation() diff --git a/external/plyer/platforms/android/sms.py b/external/plyer/platforms/android/sms.py new file mode 100644 index 0000000..8650968 --- /dev/null +++ b/external/plyer/platforms/android/sms.py @@ -0,0 +1,25 @@ +''' +Android SMS +----------- +''' + +from jnius import autoclass +from plyer.facades import Sms + +SmsManager = autoclass('android.telephony.SmsManager') + + +class AndroidSms(Sms): + + def _send(self, **kwargs): + sms = SmsManager.getDefault() + + recipient = kwargs.get('recipient') + message = kwargs.get('message') + + if sms: + sms.sendTextMessage(recipient, None, message, None, None) + + +def instance(): + return AndroidSms() diff --git a/external/plyer/platforms/android/tts.py b/external/plyer/platforms/android/tts.py new file mode 100644 index 0000000..eae2974 --- /dev/null +++ b/external/plyer/platforms/android/tts.py @@ -0,0 +1,24 @@ +from time import sleep +from jnius import autoclass +from plyer.facades import TTS +from plyer.platforms.android import activity + +Locale = autoclass('java.util.Locale') +TextToSpeech = autoclass('android.speech.tts.TextToSpeech') + + +class AndroidTextToSpeech(TTS): + def _speak(self, **kwargs): + tts = TextToSpeech(activity, None) + tts.setLanguage(Locale.US) # TODO: locale specification as option + retries = 0 # First try rarely succeeds due to some timing issue + while retries < 100 and \ + tts.speak(kwargs.get('message').encode('utf-8'), + TextToSpeech.QUEUE_FLUSH, None) == -1: + # -1 indicates error. Let's wait and then try again + sleep(0.1) + retries += 1 + + +def instance(): + return AndroidTextToSpeech() diff --git a/external/plyer/platforms/android/uniqueid.py b/external/plyer/platforms/android/uniqueid.py new file mode 100644 index 0000000..b8561de --- /dev/null +++ b/external/plyer/platforms/android/uniqueid.py @@ -0,0 +1,16 @@ +from jnius import autoclass +from plyer.platforms.android import activity +from plyer.facades import UniqueID + +Secure = autoclass('android.provider.Settings$Secure') + + +class AndroidUniqueID(UniqueID): + + def _get_uid(self): + return Secure.getString(activity.getContentResolver(), + Secure.ANDROID_ID) + + +def instance(): + return AndroidUniqueID() diff --git a/external/plyer/platforms/android/vibrator.py b/external/plyer/platforms/android/vibrator.py new file mode 100644 index 0000000..c28fe8e --- /dev/null +++ b/external/plyer/platforms/android/vibrator.py @@ -0,0 +1,48 @@ +'''Implementation Vibrator for Android.''' + +from jnius import autoclass +from plyer.facades import Vibrator +from plyer.platforms.android import activity +from plyer.platforms.android import SDK_INT + +Context = autoclass('android.content.Context') +vibrator = activity.getSystemService(Context.VIBRATOR_SERVICE) + + +class AndroidVibrator(Vibrator): + '''Android Vibrator class. + + Supported features: + * vibrate for some period of time. + * vibrate from given pattern. + * cancel vibration. + * check whether Vibrator exists. + ''' + + def _vibrate(self, time=None, **kwargs): + if vibrator: + vibrator.vibrate(int(1000 * time)) + + def _pattern(self, pattern=None, repeat=None, **kwargs): + pattern = [int(1000 * time) for time in pattern] + + if vibrator: + vibrator.vibrate(pattern, repeat) + + def _exists(self, **kwargs): + if SDK_INT >= 11: + return vibrator.hasVibrator() + elif activity.getSystemService(Context.VIBRATOR_SERVICE) is None: + raise NotImplementedError() + return True + + def _cancel(self, **kwargs): + vibrator.cancel() + + +def instance(): + '''Returns Vibrator with android features. + + :return: instance of class AndroidVibrator + ''' + return AndroidVibrator() diff --git a/external/plyer/platforms/ios/__init__.py b/external/plyer/platforms/ios/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/ios/__init__.py diff --git a/external/plyer/platforms/ios/accelerometer.py b/external/plyer/platforms/ios/accelerometer.py new file mode 100644 index 0000000..bf1ef02 --- /dev/null +++ b/external/plyer/platforms/ios/accelerometer.py @@ -0,0 +1,34 @@ +''' +iOS accelerometer +----------------- + +Taken from: http://pyobjus.readthedocs.org/en/latest/pyobjus_ios.html \ + #accessing-accelerometer +''' + +from plyer.facades import Accelerometer +from pyobjus import autoclass + + +class IosAccelerometer(Accelerometer): + + def __init__(self): + super(IosAccelerometer, self).__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setAccelerometerUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startAccelerometer() + + def _disable(self): + self.bridge.stopAccelerometer() + + def _get_acceleration(self): + return ( + self.bridge.ac_x, + self.bridge.ac_y, + self.bridge.ac_z) + + +def instance(): + return IosAccelerometer() diff --git a/external/plyer/platforms/ios/battery.py b/external/plyer/platforms/ios/battery.py new file mode 100644 index 0000000..55aa2c6 --- /dev/null +++ b/external/plyer/platforms/ios/battery.py @@ -0,0 +1,36 @@ +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework +from plyer.facades import Battery + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + + +class iOSBattery(Battery): + def __init__(self): + super(iOSBattery, self).__init__() + self.device = UIDevice.currentDevice() + + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + if(not self.device.batteryMonitoringEnabled): + self.device.setBatteryMonitoringEnabled_(True) + + if self.device.batteryState == 0: + isCharging = None + elif self.device.batteryState == 2: + isCharging = True + else: + isCharging = False + + percentage = self.device.batteryLevel * 100. + + status['isCharging'] = isCharging + status['percentage'] = percentage + + return status + + +def instance(): + return iOSBattery() diff --git a/external/plyer/platforms/ios/compass.py b/external/plyer/platforms/ios/compass.py new file mode 100644 index 0000000..6e5c935 --- /dev/null +++ b/external/plyer/platforms/ios/compass.py @@ -0,0 +1,31 @@ +''' +iOS Compass +--------------------- +''' + +from plyer.facades import Compass +from pyobjus import autoclass + + +class IosCompass(Compass): + + def __init__(self): + super(IosCompass, self).__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setMagnetometerUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startMagnetometer() + + def _disable(self): + self.bridge.stopMagnetometer() + + def _get_orientation(self): + return ( + self.bridge.mg_x, + self.bridge.mg_y, + self.bridge.mg_z) + + +def instance(): + return IosCompass() diff --git a/external/plyer/platforms/ios/email.py b/external/plyer/platforms/ios/email.py new file mode 100644 index 0000000..7e55e4e --- /dev/null +++ b/external/plyer/platforms/ios/email.py @@ -0,0 +1,41 @@ +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + +from plyer.facades import Email +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +load_framework('/System/Library/Frameworks/UIKit.framework') + +NSURL = autoclass('NSURL') +NSString = autoclass('NSString') +UIApplication = autoclass('UIApplication') + + +class iOSXEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + nsurl = NSURL.alloc().initWithString_(objc_str(uri)) + + UIApplication.sharedApplication().openURL_(nsurl) + + +def instance(): + return iOSXEmail() diff --git a/external/plyer/platforms/ios/gps.py b/external/plyer/platforms/ios/gps.py new file mode 100644 index 0000000..4d6d665 --- /dev/null +++ b/external/plyer/platforms/ios/gps.py @@ -0,0 +1,45 @@ +''' +iOS GPS +----------- +''' + +from pyobjus import autoclass, protocol +from pyobjus.dylib_manager import load_framework +from plyer.facades import GPS + +load_framework('/System/Library/Frameworks/CoreLocation.framework') +CLLocationManager = autoclass('CLLocationManager') + + +class IosGPS(GPS): + def _configure(self): + if not hasattr(self, '_location_manager'): + self._location_manager = CLLocationManager.alloc().init() + + def _start(self): + self._location_manager.delegate = self + + self._location_manager.requestWhenInUseAuthorization() + # NSLocationWhenInUseUsageDescription key must exist in Info.plist + # file. When the authorization prompt is displayed your app goes + # into pause mode and if your app doesn't support background mode + # it will crash. + self._location_manager.startUpdatingLocation() + + def _stop(self): + self._location_manager.stopUpdatingLocation() + + @protocol('CLLocationManagerDelegate') + def locationManager_didUpdateLocations_(self, manager, locations): + location = manager.location + + self.on_location( + lat=location.coordinate.a, + lon=location.coordinate.b, + speed=location.speed, + bearing=location.course, + altitude=location.altitude) + + +def instance(): + return IosGPS() diff --git a/external/plyer/platforms/ios/gyroscope.py b/external/plyer/platforms/ios/gyroscope.py new file mode 100644 index 0000000..e8b93cf --- /dev/null +++ b/external/plyer/platforms/ios/gyroscope.py @@ -0,0 +1,31 @@ +''' +iOS Gyroscope +--------------------- +''' + +from plyer.facades import Gyroscope +from pyobjus import autoclass + + +class IosGyroscope(Gyroscope): + + def __init__(self): + super(IosGyroscope, self).__init__() + self.bridge = autoclass('bridge').alloc().init() + self.bridge.motionManager.setGyroscopeUpdateInterval_(0.1) + + def _enable(self): + self.bridge.startGyroscope() + + def _disable(self): + self.bridge.stopGyroscope() + + def _get_orientation(self): + return ( + self.bridge.gy_x, + self.bridge.gy_y, + self.bridge.gy_z) + + +def instance(): + return IosGyroscope() diff --git a/external/plyer/platforms/ios/tts.py b/external/plyer/platforms/ios/tts.py new file mode 100644 index 0000000..a711483 --- /dev/null +++ b/external/plyer/platforms/ios/tts.py @@ -0,0 +1,35 @@ +from pyobjus import autoclass, objc_str +from pyobjus.dylib_manager import load_framework + +from plyer.facades import TTS + +load_framework('/System/Library/Frameworks/AVFoundation.framework') +AVSpeechUtterance = autoclass('AVSpeechUtterance') +AVSpeechSynthesizer = autoclass('AVSpeechSynthesizer') +AVSpeechSynthesisVoice = autoclass('AVSpeechSynthesisVoice') + + +class iOSTextToSpeech(TTS): + def __init__(self): + super(iOSTextToSpeech, self).__init__() + self.synth = AVSpeechSynthesizer.alloc().init() + self.voice = None + + def _set_locale(self, locale="en-US"): + self.voice = AVSpeechSynthesisVoice.voiceWithLanguage_(objc_str(locale)) + + def _speak(self, **kwargs): + message = kwargs.get('message') + + if(not self.voice): + self._set_locale() + + utterance = \ + AVSpeechUtterance.speechUtteranceWithString_(objc_str(message)) + + utterance.voice = self.voice + self.synth.speakUtterance_(utterance) + + +def instance(): + return iOSTextToSpeech() diff --git a/external/plyer/platforms/ios/uniqueid.py b/external/plyer/platforms/ios/uniqueid.py new file mode 100644 index 0000000..1587f4b --- /dev/null +++ b/external/plyer/platforms/ios/uniqueid.py @@ -0,0 +1,17 @@ +from pyobjus import autoclass +from pyobjus.dylib_manager import load_framework +from plyer.facades import UniqueID + +load_framework('/System/Library/Frameworks/UIKit.framework') +UIDevice = autoclass('UIDevice') + + +class iOSUniqueID(UniqueID): + + def _get_uid(self): + uuid = UIDevice.currentDevice().identifierForVendor.UUIDString() + return uuid.UTF8String() + + +def instance(): + return iOSUniqueID() diff --git a/external/plyer/platforms/linux/__init__.py b/external/plyer/platforms/linux/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/linux/__init__.py diff --git a/external/plyer/platforms/linux/accelerometer.py b/external/plyer/platforms/linux/accelerometer.py new file mode 100644 index 0000000..7272c33 --- /dev/null +++ b/external/plyer/platforms/linux/accelerometer.py @@ -0,0 +1,36 @@ +''' +Linux accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +import os +import glob +import re + + +class LinuxAccelerometer(Accelerometer): + + def _enable(self): + pass + + def _disable(self): + pass + + def _get_acceleration(self): + try: + pos = glob.glob("/sys/devices/platform/*/position")[0] + except IndexError: + raise Exception('Could not enable accelerometer!') + + with open(pos, "r") as p: + t = p.read() + coords = re.findall(r"[-]?\d+\.?\d*", t) + # Apparently the acceleration on sysfs goes from -1000 to 1000. + # I divide it by 100 to make it equivalent to Android. + # The negative is because the coordinates are inverted on Linux + return [float(i) / -100 for i in coords] + + +def instance(): + return LinuxAccelerometer() diff --git a/external/plyer/platforms/linux/battery.py b/external/plyer/platforms/linux/battery.py new file mode 100644 index 0000000..0cdb763 --- /dev/null +++ b/external/plyer/platforms/linux/battery.py @@ -0,0 +1,46 @@ +from subprocess import Popen, PIPE +from plyer.facades import Battery +from plyer.utils import whereis_exe + +from os import environ + + +class LinuxBattery(Battery): + def _get_state(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + status = {"isCharging": None, "percentage": None} + + # We are supporting only one battery now + dev = "/org/freedesktop/UPower/device/battery_BAT0" + upower_process = Popen(["upower", "-d", dev], + stdout=PIPE) + output = upower_process.communicate()[0] + + environ['LANG'] = old_lang + + if not output: + return status + + power_supply = percentage = None + for l in output.splitlines(): + if 'power supply' in l: + power_supply = l.rpartition(':')[-1].strip() + if 'percentage' in l: + percentage = float(l.rpartition(':')[-1].strip()[:-1]) + + if(power_supply): + status['isCharging'] = power_supply != "yes" + + status['percentage'] = percentage + + return status + + +def instance(): + import sys + if whereis_exe('upower'): + return LinuxBattery() + sys.stderr.write("upower not found.") + return Battery() diff --git a/external/plyer/platforms/linux/email.py b/external/plyer/platforms/linux/email.py new file mode 100644 index 0000000..ceb5497 --- /dev/null +++ b/external/plyer/platforms/linux/email.py @@ -0,0 +1,36 @@ +import subprocess +from urllib import quote +try: + from urllib.parse import quote +except ImportError: + from urllib import quote +from plyer.utils import whereis_exe + + +class LinuxEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + subprocess.Popen(["xdg-open", uri]) + + +def instance(): + import sys + if whereis_exe('xdg-open'): + return LinuxEmail() + sys.stderr.write("xdg-open not found.") + return Email() diff --git a/external/plyer/platforms/linux/filechooser.py b/external/plyer/platforms/linux/filechooser.py new file mode 100644 index 0000000..545487b --- /dev/null +++ b/external/plyer/platforms/linux/filechooser.py @@ -0,0 +1,249 @@ +''' +Linux file chooser +------------------ +''' + +from plyer.facades import FileChooser +from distutils.spawn import find_executable as which +import os +import subprocess as sp +import time + + +class SubprocessFileChooser(object): + '''A file chooser implementation that allows using + subprocess back-ends. + Normally you only need to override _gen_cmdline, executable, + separator and successretcode. + ''' + + executable = "" + '''The name of the executable of the back-end. + ''' + + separator = "|" + '''The separator used by the back-end. Override this for automatic + splitting, or override _split_output. + ''' + + successretcode = 0 + '''The return code which is returned when the user doesn't close the + dialog without choosing anything, or when the app doesn't crash. + ''' + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + _process = None + + def _run_command(self, cmd): + self._process = sp.Popen(cmd, stdout=sp.PIPE) + while True: + ret = self._process.poll() + if ret is not None: + if ret == self.successretcode: + out = self._process.communicate()[0].strip() + self.selection = self._split_output(out) + return self.selection + else: + return None + time.sleep(0.1) + + def _split_output(self, out): + '''This methods receives the output of the back-end and turns + it into a list of paths. + ''' + return out.split(self.separator) + + def _gen_cmdline(self): + '''Returns the command line of the back-end, based on the current + properties. You need to override this. + ''' + raise NotImplementedError() + + def run(self): + return self._run_command(self._gen_cmdline()) + + +class ZenityFileChooser(SubprocessFileChooser): + '''A FileChooser implementation using Zenity (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + ''' + + executable = "zenity" + separator = "|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [ + which(self.executable), + "--file-selection", + "--confirm-overwrite" + ] + if self.multiple: + cmdline += ["--multiple"] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += [ + "--file-filter", + "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:])) + ] + return cmdline + + +class KDialogFileChooser(SubprocessFileChooser): + '''A FileChooser implementation using KDialog (on GNU/Linux). + + Not implemented features: + * show_hidden + * preview + ''' + + executable = "kdialog" + separator = "\n" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [which(self.executable)] + + filt = [] + + for f in self.filters: + if type(f) == str: + filt += [f] + else: + filt += list(f[1:]) + + if self.mode == "dir": + cmdline += [ + "--getexistingdirectory", + (self.path if self.path else os.path.expanduser("~")) + ] + elif self.mode == "save": + cmdline += [ + "--getopenfilename", + (self.path if self.path else os.path.expanduser("~")), + " ".join(filt) + ] + else: + cmdline += [ + "--getopenfilename", + (self.path if self.path else os.path.expanduser("~")), + " ".join(filt) + ] + if self.multiple: + cmdline += ["--multiple", "--separate-output"] + if self.title: + cmdline += ["--title", self.title] + if self.icon: + cmdline += ["--icon", self.icon] + return cmdline + + +class YADFileChooser(SubprocessFileChooser): + '''A NativeFileChooser implementation using YAD (on GNU/Linux). + + Not implemented features: + * show_hidden + ''' + + executable = "yad" + separator = "|?|" + successretcode = 0 + + def _gen_cmdline(self): + cmdline = [ + which(self.executable), + "--file-selection", + "--confirm-overwrite", + "--geometry", + "800x600+150+150" + ] + if self.multiple: + cmdline += ["--multiple", "--separator", self.separator] + if self.mode == "save": + cmdline += ["--save"] + elif self.mode == "dir": + cmdline += ["--directory"] + if self.preview: + cmdline += ["--add-preview"] + if self.path: + cmdline += ["--filename", self.path] + if self.title: + cmdline += ["--name", self.title] + if self.icon: + cmdline += ["--window-icon", self.icon] + for f in self.filters: + if type(f) == str: + cmdline += ["--file-filter", f] + else: + cmdline += [ + "--file-filter", + "{name} | {flt}".format(name=f[0], flt=" ".join(f[1:])) + ] + return cmdline + +CHOOSERS = { + "gnome": ZenityFileChooser, + "kde": KDialogFileChooser, + "yad": YADFileChooser +} + + +class LinuxFileChooser(FileChooser): + '''FileChooser implementation for GNu/Linux. Accepts one additional + keyword argument, *desktop_override*, which, if set, overrides the + back-end that will be used. Set it to "gnome" for Zenity, to "kde" + for KDialog and to "yad" for YAD (Yet Another Dialog). + If set to None or not set, a default one will be picked based on + the running desktop environment and installed back-ends. + ''' + + desktop = None + if str(os.environ.get("XDG_CURRENT_DESKTOP")).lower() == "kde" \ + and which("kdialog"): + desktop = "kde" + elif which("yad"): + desktop = "yad" + elif which("zenity"): + desktop = "gnome" + + def _file_selection_dialog(self, desktop_override=desktop, **kwargs): + if not desktop_override: + desktop_override = desktop + # This means we couldn't find any back-end + if not desktop_override: + raise OSError("No back-end available. Please install one.") + + chooser = CHOOSERS[desktop_override] + c = chooser(**kwargs) + return c.run() + + +def instance(): + return LinuxFileChooser() diff --git a/external/plyer/platforms/linux/notification.py b/external/plyer/platforms/linux/notification.py new file mode 100644 index 0000000..d78f130 --- /dev/null +++ b/external/plyer/platforms/linux/notification.py @@ -0,0 +1,52 @@ +import subprocess +from plyer.facades import Notification +from plyer.utils import whereis_exe + + +class NotifySendNotification(Notification): + ''' Pops up a notification using notify-send + ''' + def _notify(self, **kwargs): + subprocess.call(["notify-send", + kwargs.get('title'), + kwargs.get('message')]) + + +class NotifyDbus(Notification): + ''' notify using dbus interface + ''' + + def _notify(self, **kwargs): + summary = kwargs.get('title', "title") + body = kwargs.get('message', "body") + app_name = kwargs.get('app_name', '') + app_icon = kwargs.get('app_icon', '') + timeout = kwargs.get('timeout', 5000) + actions = kwargs.get('actions', []) + hints = kwargs.get('hints', []) + replaces_id = kwargs.get('replaces_id', 0) + + _bus_name = 'org.freedesktop.Notifications' + _object_path = '/org/freedesktop/Notifications' + _interface_name = _bus_name + + import dbus + session_bus = dbus.SessionBus() + obj = session_bus.get_object(_bus_name, _object_path) + interface = dbus.Interface(obj, _interface_name) + interface.Notify(app_name, replaces_id, app_icon, + summary, body, actions, hints, timeout) + + +def instance(): + import sys + try: + import dbus + return NotifyDbus() + except ImportError: + sys.stderr.write("python-dbus not installed. try:" + "`sudo pip install python-dbus`.") + if whereis_exe('notify-send'): + return NotifySendNotification() + sys.stderr.write("notify-send not found.") + return Notification() diff --git a/external/plyer/platforms/linux/tts.py b/external/plyer/platforms/linux/tts.py new file mode 100644 index 0000000..0a609e1 --- /dev/null +++ b/external/plyer/platforms/linux/tts.py @@ -0,0 +1,25 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class EspeakTextToSpeech(TTS): + ''' Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +class FliteTextToSpeech(TTS): + ''' Speaks using the flite program + ''' + def _speak(self): + subprocess.call(["flite", "-t", kwargs.get('message'), "play"]) + + +def instance(): + if whereis_exe('espeak'): + return EspeakTextToSpeech() + elif whereis_exe('flite'): + return FlitetextToSpeech() + return TTS() diff --git a/external/plyer/platforms/linux/uniqueid.py b/external/plyer/platforms/linux/uniqueid.py new file mode 100644 index 0000000..f7fff89 --- /dev/null +++ b/external/plyer/platforms/linux/uniqueid.py @@ -0,0 +1,30 @@ +from subprocess import Popen, PIPE +from plyer.facades import UniqueID +from plyer.utils import whereis_exe + +from os import environ + + +class LinuxUniqueID(UniqueID): + def _get_uid(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + lshw_process = Popen(["lshw", "-quiet"], stdout=PIPE, stderr=PIPE) + grep_process = Popen(["grep", "-m1", "serial:"], + stdin=lshw_process.stdout, stdout=PIPE) + lshw_process.stdout.close() + output = grep_process.communicate()[0] + environ['LANG'] = old_lang + + if output: + return output.split()[1] + else: + return None + + +def instance(): + import sys + if whereis_exe('lshw'): + return LinuxUniqueID() + sys.stderr.write("lshw not found.") + return UniqueID() diff --git a/external/plyer/platforms/macosx/__init__.py b/external/plyer/platforms/macosx/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/macosx/__init__.py diff --git a/external/plyer/platforms/macosx/accelerometer.py b/external/plyer/platforms/macosx/accelerometer.py new file mode 100644 index 0000000..ec1fe77 --- /dev/null +++ b/external/plyer/platforms/macosx/accelerometer.py @@ -0,0 +1,25 @@ +''' +MacOSX accelerometer +--------------------- +''' + +from plyer.facades import Accelerometer +from plyer.platforms.macosx.libs import osx_motion_sensor + + +class OSXAccelerometer(Accelerometer): + def _enable(self): + try: + osx_motion_sensor.get_coord() + except: + raise Exception('Could not enable motion sensor on this macbook!') + + def _disable(self): + pass + + def _get_acceleration(self): + return osx_motion_sensor.get_coord() + + +def instance(): + return OSXAccelerometer() diff --git a/external/plyer/platforms/macosx/battery.py b/external/plyer/platforms/macosx/battery.py new file mode 100644 index 0000000..fe1c525 --- /dev/null +++ b/external/plyer/platforms/macosx/battery.py @@ -0,0 +1,47 @@ +from subprocess import Popen, PIPE +from plyer.facades import Battery +from plyer.utils import whereis_exe + +from os import environ + + +class OSXBattery(Battery): + def _get_state(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + status = {"isCharging": None, "percentage": None} + + ioreg_process = Popen(["ioreg", "-rc", "AppleSmartBattery"], + stdout=PIPE) + output = ioreg_process.communicate()[0] + + environ['LANG'] = old_lang + + if not output: + return status + + IsCharging = MaxCapacity = CurrentCapacity = None + for l in output.splitlines(): + if 'IsCharging' in l: + IsCharging = l.rpartition('=')[-1].strip() + if 'MaxCapacity' in l: + MaxCapacity = float(l.rpartition('=')[-1].strip()) + if 'CurrentCapacity' in l: + CurrentCapacity = float(l.rpartition('=')[-1].strip()) + + if (IsCharging): + status['isCharging'] = IsCharging == "Yes" + + if (CurrentCapacity and MaxCapacity): + status['percentage'] = 100. * CurrentCapacity / MaxCapacity + + return status + + +def instance(): + import sys + if whereis_exe('ioreg'): + return OSXBattery() + sys.stderr.write("ioreg not found.") + return Battery() diff --git a/external/plyer/platforms/macosx/email.py b/external/plyer/platforms/macosx/email.py new file mode 100644 index 0000000..8e29fa9 --- /dev/null +++ b/external/plyer/platforms/macosx/email.py @@ -0,0 +1,38 @@ +import subprocess + +try: + from urllib.parse import quote +except ImportError: + from urllib import quote + +from plyer.facades import Email +from plyer.utils import whereis_exe + + +class MacOSXEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + subprocess.Popen(["open", uri]) + + +def instance(): + import sys + if whereis_exe('open'): + return MacOSXEmail() + sys.stderr.write("open not found.") + return Email() diff --git a/external/plyer/platforms/macosx/filechooser.py b/external/plyer/platforms/macosx/filechooser.py new file mode 100644 index 0000000..5774cc4 --- /dev/null +++ b/external/plyer/platforms/macosx/filechooser.py @@ -0,0 +1,108 @@ +''' +Mac OS X file chooser +--------------------- +''' + +from plyer.facades import FileChooser +from pyobjus import autoclass, objc_arr, objc_str +from pyobjus.dylib_manager import load_framework, INCLUDE + +load_framework(INCLUDE.AppKit) +NSURL = autoclass('NSURL') +NSOpenPanel = autoclass('NSOpenPanel') +NSSavePanel = autoclass('NSSavePanel') +NSOKButton = 1 + + +class MacFileChooser(object): + '''A native implementation of file chooser dialogs using Apple's API + through pyobjus. + + Not implemented features: + * filters (partial, wildcards are converted to extensions if possible. + Pass the Mac-specific "use_extensions" if you can provide + Mac OS X-compatible to avoid automatic conversion) + * multiple (only for save dialog. Available in open dialog) + * icon + * preview + ''' + + mode = "open" + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + use_extensions = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + def run(self): + panel = None + if self.mode in ("open", "dir"): + panel = NSOpenPanel.openPanel() + else: + panel = NSSavePanel.savePanel() + + panel.setCanCreateDirectories_(True) + + panel.setCanChooseDirectories_(self.mode == "dir") + panel.setCanChooseFiles_(self.mode != "dir") + panel.setShowsHiddenFiles_(self.show_hidden) + + if self.title: + panel.setTitle_(objc_str(self.title)) + + if self.mode != "save" and self.multiple: + panel.setAllowsMultipleSelection_(True) + + # Mac OS X does not support wildcards unlike the other platforms. + # This tries to convert wildcards to "extensions" when possible, + # ans sets the panel to also allow other file types, just to be safe. + if len(self.filters) > 0: + filthies = [] + for f in self.filters: + if type(f) == str: + if not self.use_extensions: + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + else: + for i in f[1:]: + if not self.use_extensions: + if f.strip().endswith("*"): + continue + pystr = f.strip().split("*")[-1].split(".")[-1] + filthies.append(objc_str(pystr)) + + ftypes_arr = objc_arr(filthies) + panel.setAllowedFileTypes_(ftypes_arr) + panel.setAllowsOtherFileTypes_(not self.use_extensions) + + if self.path: + url = NSURL.fileURLWithPath_(self.path) + panel.setDirectoryURL_(url) + + if panel.runModal_(): + if self.mode == "save" or not self.multiple: + return [panel.filename().UTF8String()] + else: + return [i.UTF8String() for i in panel.filenames()] + return None + + +class MacOSXFileChooser(FileChooser): + '''FileChooser implementation for Windows, using win3all. + ''' + def _file_selection_dialog(self, **kwargs): + return MacFileChooser(**kwargs).run() + + +def instance(): + return MacOSXFileChooser() diff --git a/external/plyer/platforms/macosx/libs/__init__.py b/external/plyer/platforms/macosx/libs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/macosx/libs/__init__.py diff --git a/external/plyer/platforms/macosx/libs/osx_motion_sensor.py b/external/plyer/platforms/macosx/libs/osx_motion_sensor.py new file mode 100644 index 0000000..cd4fae4 --- /dev/null +++ b/external/plyer/platforms/macosx/libs/osx_motion_sensor.py @@ -0,0 +1,118 @@ +import ctypes +from ctypes import (Structure, cdll, sizeof, + c_int, c_int8, c_int16, c_size_t) +from ctypes.util import find_library +import platform + +ERROR_DICT = { + "0": "IOKit Framework not found, is this OSX?", + "-1": "No SMCMotionSensor service", + "-2": "No sms device", + "-3": "Could not open motion sensor device", + "-4": "Did not receive any coordinates" +} + +IOKit = cdll.LoadLibrary(find_library('IOKit')) + + +class data_structure(Structure): + _fields_ = [ + ('x', c_int16), + ('y', c_int16), + ('z', c_int16), + ('pad', c_int8 * 34), + ] + + +void_p = ctypes.POINTER(ctypes.c_int) + +kern_return_t = ctypes.c_int +KERN_SUCCESS = 0 +KERN_FUNC = 5 # SMC Motion Sensor on MacBook Pro + +mach_port_t = void_p +MACH_PORT_NULL = 0 + +io_object_t = ctypes.c_int +io_object_t = ctypes.c_int +io_iterator_t = void_p +io_object_t = void_p +io_connect_t = void_p +IOItemCount = ctypes.c_uint + +CFMutableDictionaryRef = void_p + + +def is_os_64bit(): + return platform.machine().endswith('64') + + +def read_sms(): + result = kern_return_t() + masterPort = mach_port_t() + + result = IOKit.IOMasterPort(MACH_PORT_NULL, ctypes.byref(masterPort)) + + IOKit.IOServiceMatching.restype = CFMutableDictionaryRef + matchingDictionary = IOKit.IOServiceMatching("SMCMotionSensor") + + iterator = io_iterator_t() + result = IOKit.IOServiceGetMatchingServices(masterPort, matchingDictionary, + ctypes.byref(iterator)) + + if (result != KERN_SUCCESS): + raise ("No coordinates received!") + return -1, None + + IOKit.IOIteratorNext.restype = io_object_t + smsDevice = IOKit.IOIteratorNext(iterator) + + if not smsDevice: + return -2, None + + dataPort = io_connect_t() + result = IOKit.IOServiceOpen(smsDevice, IOKit.mach_task_self(), 0, + ctypes.byref(dataPort)) + + if (result != KERN_SUCCESS): + return -3, None + + inStructure = data_structure() + outStructure = data_structure() + + if(is_os_64bit() or hasattr(IOKit, 'IOConnectCallStructMethod')): + structureInSize = IOItemCount(sizeof(data_structure)) + structureOutSize = c_size_t(sizeof(data_structure)) + + result = IOKit.IOConnectCallStructMethod(dataPort, KERN_FUNC, + ctypes.byref(inStructure), structureInSize, + ctypes.byref(outStructure), ctypes.byref(structureOutSize)) + else: + structureInSize = IOItemCount(sizeof(data_structure)) + structureOutSize = IOItemCount(sizeof(data_structure)) + + result = IOConnectMethodStructureIStructureO(dataPort, KERN_FUNC, + structureInSize, ctypes.byref(structureOutSize), + ctypes.byref(inStructure), ctypes.byref(outStructure)) + + IOKit.IOServiceClose(dataPort) + + if (result != KERN_SUCCESS): + return -4, None + + return 1, outStructure + + +def get_coord(): + if not IOKit: + raise Exception(ERROR_DICT["0"]) + + ret, data = read_sms() + + if (ret > 0): + if(data.x): + return (data.x, data.y, data.z) + else: + return (None, None, None) + else: + raise Exception(ERROR_DICT[str(ret)]) diff --git a/external/plyer/platforms/macosx/notification.py b/external/plyer/platforms/macosx/notification.py new file mode 100644 index 0000000..a52ebe3 --- /dev/null +++ b/external/plyer/platforms/macosx/notification.py @@ -0,0 +1,27 @@ +from plyer.facades import Notification +import Foundation +import objc +import AppKit + + +class OSXNotification(Notification): + def _notify(self, **kwargs): + NSUserNotification = objc.lookUpClass('NSUserNotification') + NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter') + notification = NSUserNotification.alloc().init() + notification.setTitle_(kwargs.get('title').encode('utf-8')) + #notification.setSubtitle_(str(subtitle)) + notification.setInformativeText_(kwargs.get('message').encode('utf-8')) + notification.setSoundName_("NSUserNotificationDefaultSoundName") + #notification.setHasActionButton_(False) + #notification.setOtherButtonTitle_("View") + #notification.setUserInfo_({"action":"open_url", "value":url}) + NSUserNotificationCenter.defaultUserNotificationCenter() \ + .setDelegate_(self) + NSUserNotificationCenter.defaultUserNotificationCenter() \ + .scheduleNotification_(notification) + + +def instance(): + return OSXNotification() + diff --git a/external/plyer/platforms/macosx/tts.py b/external/plyer/platforms/macosx/tts.py new file mode 100644 index 0000000..755e820 --- /dev/null +++ b/external/plyer/platforms/macosx/tts.py @@ -0,0 +1,25 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class NativeSayTextToSpeech(TTS): + '''Speaks using the native OSX 'say' command + ''' + def _speak(self, **kwargs): + subprocess.call(["say", kwargs.get('message')]) + + +class EspeakTextToSpeech(TTS): + '''Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +def instance(): + if whereis_exe('say'): + return NativeSayTextToSpeech() + elif whereis_exe('espeak'): + return EspeakTextToSpeech() + return TTS() diff --git a/external/plyer/platforms/macosx/uniqueid.py b/external/plyer/platforms/macosx/uniqueid.py new file mode 100644 index 0000000..51ba169 --- /dev/null +++ b/external/plyer/platforms/macosx/uniqueid.py @@ -0,0 +1,32 @@ +from subprocess import Popen, PIPE +from plyer.facades import UniqueID +from plyer.utils import whereis_exe + +from os import environ + + +class OSXUniqueID(UniqueID): + def _get_uid(self): + old_lang = environ.get('LANG') + environ['LANG'] = 'C' + + ioreg_process = Popen(["ioreg", "-l"], stdout=PIPE) + grep_process = Popen(["grep", "IOPlatformSerialNumber"], + stdin=ioreg_process.stdout, stdout=PIPE) + ioreg_process.stdout.close() + output = grep_process.communicate()[0] + + environ['LANG'] = old_lang + + if output: + return output.split()[3][1:-1] + else: + return None + + +def instance(): + import sys + if whereis_exe('ioreg'): + return OSXUniqueID() + sys.stderr.write("ioreg not found.") + return UniqueID() diff --git a/external/plyer/platforms/win/__init__.py b/external/plyer/platforms/win/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/win/__init__.py diff --git a/external/plyer/platforms/win/battery.py b/external/plyer/platforms/win/battery.py new file mode 100644 index 0000000..04006f8 --- /dev/null +++ b/external/plyer/platforms/win/battery.py @@ -0,0 +1,21 @@ +from plyer.platforms.win.libs.batterystatus import battery_status +from plyer.facades import Battery + + +class WinBattery(Battery): + def _get_state(self): + status = {"isCharging": None, "percentage": None} + + query = battery_status() + + if (not query): + return status + + status["isCharging"] = query["BatteryFlag"] == 8 + status["percentage"] = query["BatteryLifePercent"] + + return status + + +def instance(): + return WinBattery() diff --git a/external/plyer/platforms/win/email.py b/external/plyer/platforms/win/email.py new file mode 100644 index 0000000..e33f5cf --- /dev/null +++ b/external/plyer/platforms/win/email.py @@ -0,0 +1,34 @@ +import os +try: + from urllib.parse import quote +except ImportError: + from urllib import quote +from plyer.facades import Email + + +class WindowsEmail(Email): + def _send(self, **kwargs): + recipient = kwargs.get('recipient') + subject = kwargs.get('subject') + text = kwargs.get('text') + + uri = "mailto:" + if recipient: + uri += str(recipient) + if subject: + uri += "?" if not "?" in uri else "&" + uri += "subject=" + uri += quote(str(subject)) + if text: + uri += "?" if not "?" in uri else "&" + uri += "body=" + uri += quote(str(text)) + + try: + os.startfile(uri) + except WindowsError: + print("Warning: unable to find a program able to send emails.") + + +def instance(): + return WindowsEmail() diff --git a/external/plyer/platforms/win/filechooser.py b/external/plyer/platforms/win/filechooser.py new file mode 100644 index 0000000..4d284dc --- /dev/null +++ b/external/plyer/platforms/win/filechooser.py @@ -0,0 +1,112 @@ +''' +Windows file chooser +-------------------- +''' + +from plyer.facades import FileChooser +from win32com.shell import shell, shellcon +import os +import win32gui +import win32con +import pywintypes + + +class Win32FileChooser(object): + '''A native implementation of NativeFileChooser using the + Win32 API on Windows. + + Not Implemented features (all dialogs): + * preview + * icon + + Not implemented features (in directory selection only - it's limited + by Windows itself): + * preview + * window-icon + * multiple + * show_hidden + * filters + * path + ''' + + path = None + multiple = False + filters = [] + preview = False + title = None + icon = None + show_hidden = False + + def __init__(self, **kwargs): + # Simulate Kivy's behavior + for i in kwargs: + setattr(self, i, kwargs[i]) + + def run(self): + try: + if self.mode != "dir": + args = {} + + if self.path: + args["InitialDir"] = os.path.dirname(self.path) + path = os.path.splitext(os.path.dirname(self.path)) + args["File"] = path[0] + args["DefExt"] = path[1] + args["Title"] = self.title if self.title else "Pick a file..." + args["CustomFilter"] = 'Other file types\x00*.*\x00' + args["FilterIndex"] = 1 + + filters = "" + for f in self.filters: + if type(f) == str: + filters += (f + "\x00") * 2 + else: + filters += f[0] + "\x00" + ";".join(f[1:]) + "\x00" + args["Filter"] = filters + + flags = (win32con.OFN_EXTENSIONDIFFERENT | + win32con.OFN_OVERWRITEPROMPT) + if self.multiple: + flags |= win32con.OFN_ALLOWmultiple | win32con.OFN_EXPLORER + if self.show_hidden: + flags |= win32con.OFN_FORCESHOWHIDDEN + args["Flags"] = flags + + if self.mode == "open": + self.fname, _, _ = win32gui.GetOpenFileNameW(**args) + elif self.mode == "save": + self.fname, _, _ = win32gui.GetSaveFileNameW(**args) + + if self.fname: + if self.multiple: + seq = str(self.fname).split("\x00") + dir_n, base_n = seq[0], seq[1:] + self.selection = [os.path.join(dir_n, i) + for i in base_n] + else: + self.selection = str(self.fname).split("\x00") + else: + # From http://goo.gl/UDqCqo + pidl, display_name, image_list = shell.SHBrowseForFolder( + win32gui.GetDesktopWindow(), + None, + self.title if self.title else "Pick a folder...", + 0, None, None + ) + self.selection = [str(shell.SHGetPathFromIDList(pidl))] + + return self.selection + except (RuntimeError, pywintypes.error): + return None + + +class WinFileChooser(FileChooser): + '''FileChooser implementation for Windows, using win3all. + ''' + + def _file_selection_dialog(self, **kwargs): + return Win32FileChooser(**kwargs).run() + + +def instance(): + return WinFileChooser() diff --git a/external/plyer/platforms/win/libs/__init__.py b/external/plyer/platforms/win/libs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/external/plyer/platforms/win/libs/__init__.py diff --git a/external/plyer/platforms/win/libs/balloontip.py b/external/plyer/platforms/win/libs/balloontip.py new file mode 100644 index 0000000..171b25f --- /dev/null +++ b/external/plyer/platforms/win/libs/balloontip.py @@ -0,0 +1,145 @@ +# -- coding: utf-8 -- + +__all__ = ('WindowsBalloonTip', 'balloon_tip') + + +import time +import ctypes +from plyer.platforms.win.libs import win_api_defs +from plyer.compat import PY2 +from threading import RLock + + +WS_OVERLAPPED = 0x00000000 +WS_SYSMENU = 0x00080000 +WM_DESTROY = 2 +CW_USEDEFAULT = 8 + +LR_LOADFROMFILE = 16 +LR_DEFAULTSIZE = 0x0040 +IDI_APPLICATION = 32512 +IMAGE_ICON = 1 + +NOTIFYICON_VERSION_4 = 4 +NIM_ADD = 0 +NIM_MODIFY = 1 +NIM_DELETE = 2 +NIM_SETVERSION = 4 +NIF_MESSAGE = 1 +NIF_ICON = 2 +NIF_TIP = 4 +NIF_INFO = 0x10 +NIIF_USER = 4 +NIIF_LARGE_ICON = 0x20 + + +class WindowsBalloonTip(object): + + _class_atom = 0 + _wnd_class_ex = None + _hwnd = None + _hicon = None + _balloon_icon = None + _notify_data = None + _count = 0 + _lock = RLock() + + @staticmethod + def _get_unique_id(): + WindowsBalloonTip._lock.acquire() + val = WindowsBalloonTip._count + WindowsBalloonTip._count += 1 + WindowsBalloonTip._lock.release() + return val + + def __init__(self, title, message, app_name, app_icon='', timeout=10): + ''' app_icon if given is a icon file. + ''' + + wnd_class_ex = win_api_defs.get_WNDCLASSEXW() + class_name = 'PlyerTaskbar' + str(WindowsBalloonTip._get_unique_id()) + if PY2: + class_name = class_name.decode('utf8') + wnd_class_ex.lpszClassName = class_name + # keep ref to it as long as window is alive + wnd_class_ex.lpfnWndProc =\ + win_api_defs.WindowProc(win_api_defs.DefWindowProcW) + wnd_class_ex.hInstance = win_api_defs.GetModuleHandleW(None) + if wnd_class_ex.hInstance is None: + raise Exception('Could not get windows module instance.') + class_atom = win_api_defs.RegisterClassExW(wnd_class_ex) + if class_atom == 0: + raise Exception('Could not register the PlyerTaskbar class.') + self._class_atom = class_atom + self._wnd_class_ex = wnd_class_ex + + # create window + self._hwnd = win_api_defs.CreateWindowExW(0, class_atom, + '', WS_OVERLAPPED, 0, 0, CW_USEDEFAULT, + CW_USEDEFAULT, None, None, wnd_class_ex.hInstance, None) + if self._hwnd is None: + raise Exception('Could not get create window.') + win_api_defs.UpdateWindow(self._hwnd) + + # load icon + if app_icon: + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + hicon = win_api_defs.LoadImageW(None, app_icon, IMAGE_ICON, 0, 0, + icon_flags) + if hicon is None: + raise Exception('Could not load icon {}'. + format(icon_path_name)) + self._balloon_icon = self._hicon = hicon + else: + self._hicon = win_api_defs.LoadIconW(None, + ctypes.cast(IDI_APPLICATION, win_api_defs.LPCWSTR)) + self.notify(title, message, app_name) + if timeout: + time.sleep(timeout) + + def __del__(self): + self.remove_notify() + if self._hicon is not None: + win_api_defs.DestroyIcon(self._hicon) + if self._wnd_class_ex is not None: + win_api_defs.UnregisterClassW(self._class_atom, + self._wnd_class_ex.hInstance) + if self._hwnd is not None: + win_api_defs.DestroyWindow(self._hwnd) + + def notify(self, title, message, app_name): + ''' Displays a balloon in the systray. Can be called multiple times + with different parameter values. + ''' + self.remove_notify() + # add icon and messages to window + hicon = self._hicon + flags = NIF_TIP | NIF_INFO + icon_flag = 0 + if hicon is not None: + flags |= NIF_ICON + # if icon is default app's one, don't display it in message + if self._balloon_icon is not None: + icon_flag = NIIF_USER | NIIF_LARGE_ICON + notify_data = win_api_defs.get_NOTIFYICONDATAW(0, self._hwnd, + id(self), flags, 0, hicon, app_name, 0, 0, message, + NOTIFYICON_VERSION_4, title, icon_flag, win_api_defs.GUID(), + self._balloon_icon) + + self._notify_data = notify_data + if not win_api_defs.Shell_NotifyIconW(NIM_ADD, notify_data): + raise Exception('Shell_NotifyIconW failed.') + if not win_api_defs.Shell_NotifyIconW(NIM_SETVERSION, + notify_data): + raise Exception('Shell_NotifyIconW failed.') + + def remove_notify(self): + '''Removes the notify balloon, if displayed. + ''' + if self._notify_data is not None: + win_api_defs.Shell_NotifyIconW(NIM_DELETE, self._notify_data) + self._notify_data = None + + +def balloon_tip(**kwargs): + WindowsBalloonTip(**kwargs) diff --git a/external/plyer/platforms/win/libs/batterystatus.py b/external/plyer/platforms/win/libs/batterystatus.py new file mode 100644 index 0000000..ddb22cc --- /dev/null +++ b/external/plyer/platforms/win/libs/batterystatus.py @@ -0,0 +1,13 @@ +__all__ = ('battery_status') + + +import ctypes +from plyer.platforms.win.libs import win_api_defs + + +def battery_status(): + status = win_api_defs.SYSTEM_POWER_STATUS() + if not win_api_defs.GetSystemPowerStatus(ctypes.pointer(status)): + raise Exception('Could not get system power status.') + + return dict((field, getattr(status, field)) for field, _ in status._fields_) diff --git a/external/plyer/platforms/win/libs/win_api_defs.py b/external/plyer/platforms/win/libs/win_api_defs.py new file mode 100644 index 0000000..7aed430 --- /dev/null +++ b/external/plyer/platforms/win/libs/win_api_defs.py @@ -0,0 +1,200 @@ +''' Defines ctypes windows api. +''' + +__all__ = ('GUID', 'get_DLLVERSIONINFO', 'MAKEDLLVERULL', + 'get_NOTIFYICONDATAW', 'CreateWindowExW', 'WindowProc', + 'DefWindowProcW', 'get_WNDCLASSEXW', 'GetModuleHandleW', + 'RegisterClassExW', 'UpdateWindow', 'LoadImageW', + 'Shell_NotifyIconW', 'DestroyIcon', 'UnregisterClassW', + 'DestroyWindow', 'LoadIconW') + +import ctypes +from ctypes import Structure, windll, sizeof, POINTER, WINFUNCTYPE +from ctypes.wintypes import (DWORD, HICON, HWND, UINT, WCHAR, WORD, BYTE, + LPCWSTR, INT, LPVOID, HINSTANCE, HMENU, LPARAM, WPARAM, + HBRUSH, HMODULE, ATOM, BOOL, HANDLE) +LRESULT = LPARAM +HRESULT = HANDLE +HCURSOR = HICON + + +class GUID(Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8) + ] + + +class DLLVERSIONINFO(Structure): + _fields_ = [ + ('cbSize', DWORD), + ('dwMajorVersion', DWORD), + ('dwMinorVersion', DWORD), + ('dwBuildNumber', DWORD), + ('dwPlatformID', DWORD), + ] + + +def get_DLLVERSIONINFO(*largs): + version_info = DLLVERSIONINFO(*largs) + version_info.cbSize = sizeof(DLLVERSIONINFO) + return version_info + + +def MAKEDLLVERULL(major, minor, build, sp): + return (major << 48) | (minor << 32) | (build << 16) | sp + + +NOTIFYICONDATAW_fields = [ + ("cbSize", DWORD), + ("hWnd", HWND), + ("uID", UINT), + ("uFlags", UINT), + ("uCallbackMessage", UINT), + ("hIcon", HICON), + ("szTip", WCHAR * 128), + ("dwState", DWORD), + ("dwStateMask", DWORD), + ("szInfo", WCHAR * 256), + ("uVersion", UINT), + ("szInfoTitle", WCHAR * 64), + ("dwInfoFlags", DWORD), + ("guidItem", GUID), + ("hBalloonIcon", HICON), +] + + +class NOTIFYICONDATAW(Structure): + _fields_ = NOTIFYICONDATAW_fields[:] + + +class NOTIFYICONDATAW_V3(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-1] + + +class NOTIFYICONDATAW_V2(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-2] + + +class NOTIFYICONDATAW_V1(Structure): + _fields_ = NOTIFYICONDATAW_fields[:6] + + +NOTIFYICONDATA_V3_SIZE = sizeof(NOTIFYICONDATAW_V3) +NOTIFYICONDATA_V2_SIZE = sizeof(NOTIFYICONDATAW_V2) +NOTIFYICONDATA_V1_SIZE = sizeof(NOTIFYICONDATAW_V1) + + +def get_NOTIFYICONDATAW(*largs): + notify_data = NOTIFYICONDATAW(*largs) + + # get shell32 version to find correct NOTIFYICONDATAW size + DllGetVersion = windll.Shell32.DllGetVersion + DllGetVersion.argtypes = [POINTER(DLLVERSIONINFO)] + DllGetVersion.restype = HRESULT + + version = get_DLLVERSIONINFO() + if DllGetVersion(version): + raise Exception('Cannot get Windows version numbers.') + v = MAKEDLLVERULL(version.dwMajorVersion, version.dwMinorVersion, + version.dwBuildNumber, version.dwPlatformID) + + # from the version info find the NOTIFYICONDATA size + if v >= MAKEDLLVERULL(6, 0, 6, 0): + notify_data.cbSize = sizeof(NOTIFYICONDATAW) + elif v >= MAKEDLLVERULL(6, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V3_SIZE + elif v >= MAKEDLLVERULL(5, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V2_SIZE + else: + notify_data.cbSize = NOTIFYICONDATA_V1_SIZE + return notify_data + + +CreateWindowExW = windll.User32.CreateWindowExW +CreateWindowExW.argtypes = [DWORD, ATOM, LPCWSTR, DWORD, INT, INT, INT, INT, + HWND, HMENU, HINSTANCE, LPVOID] +CreateWindowExW.restype = HWND + +GetModuleHandleW = windll.Kernel32.GetModuleHandleW +GetModuleHandleW.argtypes = [LPCWSTR] +GetModuleHandleW.restype = HMODULE + +WindowProc = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) +DefWindowProcW = windll.User32.DefWindowProcW +DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM] +DefWindowProcW.restype = LRESULT + + +class WNDCLASSEXW(Structure): + _fields_ = [ + ('cbSize', UINT), + ('style', UINT), + ('lpfnWndProc', WindowProc), + ('cbClsExtra', INT), + ('cbWndExtra', INT), + ('hInstance', HINSTANCE), + ('hIcon', HICON), + ('hCursor', HCURSOR), + ('hbrBackground', HBRUSH), + ('lpszMenuName', LPCWSTR), + ('lpszClassName', LPCWSTR), + ('hIconSm', HICON), + ] + + +def get_WNDCLASSEXW(*largs): + wnd_class = WNDCLASSEXW(*largs) + wnd_class.cbSize = sizeof(WNDCLASSEXW) + return wnd_class + +RegisterClassExW = windll.User32.RegisterClassExW +RegisterClassExW.argtypes = [POINTER(WNDCLASSEXW)] +RegisterClassExW.restype = ATOM + +UpdateWindow = windll.User32.UpdateWindow +UpdateWindow.argtypes = [HWND] +UpdateWindow.restype = BOOL + +LoadImageW = windll.User32.LoadImageW +LoadImageW.argtypes = [HINSTANCE, LPCWSTR, UINT, INT, INT, UINT] +LoadImageW.restype = HANDLE + +Shell_NotifyIconW = windll.Shell32.Shell_NotifyIconW +Shell_NotifyIconW.argtypes = [DWORD, POINTER(NOTIFYICONDATAW)] +Shell_NotifyIconW.restype = BOOL + +DestroyIcon = windll.User32.DestroyIcon +DestroyIcon.argtypes = [HICON] +DestroyIcon.restype = BOOL + +UnregisterClassW = windll.User32.UnregisterClassW +UnregisterClassW.argtypes = [ATOM, HINSTANCE] +UnregisterClassW.restype = BOOL + +DestroyWindow = windll.User32.DestroyWindow +DestroyWindow.argtypes = [HWND] +DestroyWindow.restype = BOOL + +LoadIconW = windll.User32.LoadIconW +LoadIconW.argtypes = [HINSTANCE, LPCWSTR] +LoadIconW.restype = HICON + + +class SYSTEM_POWER_STATUS(ctypes.Structure): + _fields_ = [ + ('ACLineStatus', BYTE), + ('BatteryFlag', BYTE), + ('BatteryLifePercent', BYTE), + ('Reserved1', BYTE), + ('BatteryLifeTime', DWORD), + ('BatteryFullLifeTime', DWORD), + ] + +SystemPowerStatusP = ctypes.POINTER(SYSTEM_POWER_STATUS) + +GetSystemPowerStatus = ctypes.windll.kernel32.GetSystemPowerStatus +GetSystemPowerStatus.argtypes = [SystemPowerStatusP] +GetSystemPowerStatus.restype = BOOL diff --git a/external/plyer/platforms/win/notification.py b/external/plyer/platforms/win/notification.py new file mode 100644 index 0000000..ea46e08 --- /dev/null +++ b/external/plyer/platforms/win/notification.py @@ -0,0 +1,13 @@ +from threading import Thread as thread + +from plyer.facades import Notification +from plyer.platforms.win.libs.balloontip import balloon_tip + + +class WindowsNotification(Notification): + def _notify(self, **kwargs): + thread(target=balloon_tip, kwargs=kwargs).start() + + +def instance(): + return WindowsNotification() diff --git a/external/plyer/platforms/win/tts.py b/external/plyer/platforms/win/tts.py new file mode 100644 index 0000000..e2539c3 --- /dev/null +++ b/external/plyer/platforms/win/tts.py @@ -0,0 +1,16 @@ +import subprocess +from plyer.facades import TTS +from plyer.utils import whereis_exe + + +class EspeakTextToSpeech(TTS): + ''' Speaks using the espeak program + ''' + def _speak(self, **kwargs): + subprocess.call(["espeak", kwargs.get('message')]) + + +def instance(): + if whereis_exe('espeak.exe'): + return EspeakTextToSpeech() + return TTS() diff --git a/external/plyer/platforms/win/uniqueid.py b/external/plyer/platforms/win/uniqueid.py new file mode 100644 index 0000000..bfcf996 --- /dev/null +++ b/external/plyer/platforms/win/uniqueid.py @@ -0,0 +1,21 @@ +try: + import _winreg as regedit +except: + try: + import winreg as regedit + except: + raise NotImplemented() + +from plyer.facades import UniqueID + + +class WinUniqueID(UniqueID): + def _get_uid(self): + hKey = regedit.OpenKey(regedit.HKEY_LOCAL_MACHINE, + r"SOFTWARE\\Microsoft\\Cryptography") + value, _ = regedit.QueryValueEx(hKey, "MachineGuid") + return value + + +def instance(): + return WinUniqueID() diff --git a/external/plyer/tools/pep8checker/pep8.py b/external/plyer/tools/pep8checker/pep8.py new file mode 100644 index 0000000..63a78e2 --- /dev/null +++ b/external/plyer/tools/pep8checker/pep8.py @@ -0,0 +1,1956 @@ +#!/usr/bin/env python +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006 Johann C. Rocholl <johann@rocholl.net> +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +r""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements +900 syntax error + +You can add checks to this program by writing plugins. Each plugin is +a simple function that is called for each line of source code, either +physical or logical. + +Physical line: +- Raw line of text from the input file. + +Logical line: +- Multi-line statements converted to a single line. +- Stripped left and right. +- Contents of strings replaced with 'xxx' of same length. +- Comments removed. + +The check function requests physical or logical lines by the name of +the first argument: + +def maximum_line_length(physical_line) +def extraneous_whitespace(logical_line) +def blank_lines(logical_line, blank_lines, indent_level, line_number) + +The last example above demonstrates how check plugins can request +additional information with extra arguments. All attributes of the +Checker object are available. Some examples: + +lines: a list of the raw lines from the input file +tokens: the tokens that contribute to this logical line +line_number: line number in the input file +blank_lines: blank lines before this one +indent_char: first indentation character in this file (' ' or '\t') +indent_level: indentation (with tabs expanded to multiples of 8) +previous_indent_level: indentation on previous line +previous_logical: previous logical line + +The docstring of each check function shall be the relevant part of +text from PEP 8. It is printed if the user enables --show-pep8. +Several docstrings contain examples directly from the PEP 8 document. + +Okay: spam(ham[1], {eggs: 2}) +E201: spam( ham[1], {eggs: 2}) + +These examples are verified automatically when pep8.py is run with the +--doctest option. You can add examples for your own check functions. +The format is simple: "Okay" or error/warning code followed by colon +and space, the rest of the line is example source code. If you put 'r' +before the docstring, you can use \n for newline, \t for tab and \s +for space. + +""" + +__version__ = '1.3.3' + +import os +import sys +import re +import time +import inspect +import keyword +import tokenize +from optparse import OptionParser +from fnmatch import fnmatch +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + from io import TextIOWrapper + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' +DEFAULT_IGNORE = 'E24' +if sys.platform == 'win32': + DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') +else: + DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or + os.path.expanduser('~/.config'), 'pep8') +MAX_LINE_LENGTH = 80 +REPORT_FORMAT = { + 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', + 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', +} + + +SINGLETONS = frozenset(['False', 'None', 'True']) +KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS +BINARY_OPERATORS = frozenset([ + '**=', '*=', '+=', '-=', '!=', '<>', + '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=', + '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<']) +UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) +OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS +WHITESPACE = frozenset(' \t') +SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, + tokenize.INDENT, tokenize.DEDENT]) +BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)') +RERAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*,\s*\w+\s*,\s*\w+') +SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)') +ERRORCODE_REGEX = re.compile(r'[EW]\d{3}') +DOCSTRING_REGEX = re.compile(r'u?r?["\']') +EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') +WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') +COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') +COMPARE_TYPE_REGEX = re.compile(r'([=!]=|is|is\s+not)\s*type(?:s\.(\w+)Type' + r'|\(\s*(\(\s*\)|[^)]*[^ )])\s*\))') +KEYWORD_REGEX = re.compile(r'(?:[^\s])(\s*)\b(?:%s)\b(\s*)' % + r'|'.join(KEYWORDS)) +OPERATOR_REGEX = re.compile(r'(?:[^\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)') +LAMBDA_REGEX = re.compile(r'\blambda\b') +HUNK_REGEX = re.compile(r'^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$') + +# Work around Python < 2.6 behaviour, which does not generate NL after +# a comment which is on a line by itself. +COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n' + + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101 indentation contains mixed spaces and tabs" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if '\t' in indent: + return indent.index('\t'), "W191 indentation contains tabs" + + +def trailing_whitespace(physical_line): + r""" + JCR: Trailing whitespace is superfluous. + FBM: Except when it occurs as part of a blank line (i.e. the line is + nothing but whitespace). According to Python docs[1] a line with only + whitespace is considered a blank line, and is to be ignored. However, + matching a blank line to its indentation level avoids mistakenly + terminating a multi-line statement (e.g. class declaration) when + pasting code into the standard Python interpreter. + + [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines + + The warning returned varies on whether the line itself is blank, for easier + filtering for those who want to indent their blank lines. + + Okay: spam(1) + W291: spam(1)\s + W293: class Foo(object):\n \n bang = 12 + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip(' \t\v') + if physical_line != stripped: + if stripped: + return len(stripped), "W291 trailing whitespace" + else: + return 0, "W293 blank line contains whitespace" + + +#def trailing_blank_lines(physical_line, lines, line_number): +# r""" +# JCR: Trailing blank lines are superfluous. +# +# Okay: spam(1) +# W391: spam(1)\n +# """ +# if not physical_line.rstrip() and line_number == len(lines): +# return 0, "W391 blank line at end of file" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + + Reports warning W292. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292 no newline at end of file" + + +def maximum_line_length(physical_line, max_line_length): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + + Reports error E501. + """ + line = physical_line.rstrip() + length = len(line) + if length > max_line_length: + if hasattr(line, 'decode'): # Python 2 + # The line could contain multi-byte characters + try: + length = len(line.decode('utf-8')) + except UnicodeError: + pass + if length > max_line_length: + return (max_line_length, "E501 line too long " + "(%d > %d characters)" % (length, max_line_length)) + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, previous_indent_level): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number == 1: + return # Don't expect blank lines before the first line + if previous_logical.startswith('@'): + if blank_lines: + yield 0, "E304 blank lines found after function decorator" + elif blank_lines > 2 or (indent_level and blank_lines == 2): + yield 0, "E303 too many blank lines (%d)" % blank_lines + elif logical_line.startswith(('def ', 'class ', '@')): + if indent_level: + if not (blank_lines or previous_indent_level < indent_level or + DOCSTRING_REGEX.match(previous_logical)): + yield 0, "E301 expected 1 blank line, found 0" + elif blank_lines != 2: + yield 0, "E302 expected 2 blank lines, found %d" % blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): + text = match.group() + char = text.strip() + found = match.start() + if text == char + ' ': + # assert char in '([{' + yield found + 1, "E201 whitespace after '%s'" % char + elif line[found - 1] != ',': + code = ('E202' if char in '}])' else 'E203') # if char in ',;:' + yield found, "%s whitespace before '%s'" % (code, char) + + +def whitespace_around_keywords(logical_line): + r""" + Avoid extraneous whitespace around keywords. + + Okay: True and False + E271: True and False + E272: True and False + E273: True and\tFalse + E274: True\tand False + """ + for match in KEYWORD_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E274 tab before keyword" + elif len(before) > 1: + yield match.start(1), "E272 multiple spaces before keyword" + + if '\t' in after: + yield match.start(2), "E273 tab after keyword" + elif len(after) > 1: + yield match.start(2), "E271 multiple spaces after keyword" + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']'): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + yield index, "E231 missing whitespace after '%s'" % char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + yield 0, "E111 indentation is not a multiple of four" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + yield 0, "E112 expected an indented block" + if indent_level > previous_indent_level and not indent_expect: + yield 0, "E113 unexpected indentation" + + +def continuation_line_indentation(logical_line, tokens, indent_level, verbose): + r""" + Continuation lines should align wrapped elements either vertically using + Python's implicit line joining inside parentheses, brackets and braces, or + using a hanging indent. + + When using a hanging indent the following considerations should be applied: + + - there should be no arguments on the first line, and + + - further indentation should be used to clearly distinguish itself as a + continuation line. + + Okay: a = (\n) + E123: a = (\n ) + + Okay: a = (\n 42) + E121: a = (\n 42) + E122: a = (\n42) + E123: a = (\n 42\n ) + E124: a = (24,\n 42\n) + E125: if (a or\n b):\n pass + E126: a = (\n 42) + E127: a = (24,\n 42) + E128: a = (24,\n 42) + """ + first_row = tokens[0][2][0] + nrows = 1 + tokens[-1][2][0] - first_row + if nrows == 1: + return + + # indent_next tells us whether the next block is indented; assuming + # that it is indented by 4 spaces, then we should not allow 4-space + # indents on the final continuation line; in turn, some other + # indents are allowed to have an extra 4 spaces. + indent_next = logical_line.endswith(':') + + row = depth = 0 + # remember how many brackets were opened on each line + parens = [0] * nrows + # relative indents of physical lines + rel_indent = [0] * nrows + # visual indents + indent = [indent_level] + indent_chances = {} + last_indent = (0, 0) + if verbose >= 3: + print((">>> " + tokens[0][4].rstrip())) + + for token_type, text, start, end, line in tokens: + newline = row < start[0] - first_row + if newline: + row = start[0] - first_row + newline = (not last_token_multiline and + token_type not in (tokenize.NL, tokenize.NEWLINE)) + + if newline: + # this is the beginning of a continuation line. + last_indent = start + if verbose >= 3: + print(("... " + line.rstrip())) + + # record the initial indent. + rel_indent[row] = start[1] - indent_level + + if depth: + # a bracket expression in a continuation line. + # find the line that it was opened on + for open_row in range(row - 1, -1, -1): + if parens[open_row]: + break + else: + # an unbracketed continuation line (ie, backslash) + open_row = 0 + hang = rel_indent[row] - rel_indent[open_row] + visual_indent = indent_chances.get(start[1]) + + if token_type == tokenize.OP and text in ']})': + # this line starts with a closing bracket + if indent[depth]: + if start[1] != indent[depth]: + yield (start, 'E124 closing bracket does not match ' + 'visual indentation') + elif hang: + yield (start, 'E123 closing bracket does not match ' + 'indentation of opening bracket\'s line') + elif visual_indent is True: + # visual indent is verified + if not indent[depth]: + indent[depth] = start[1] + elif visual_indent in (text, str): + # ignore token lined up with matching one from a previous line + pass + elif indent[depth] and start[1] < indent[depth]: + # visual indent is broken + yield (start, 'E128 continuation line ' + 'under-indented for visual indent') + elif hang == 4 or (indent_next and rel_indent[row] == 8): + # hanging indent is verified + pass + else: + # indent is broken + if hang <= 0: + error = 'E122', 'missing indentation or outdented' + elif indent[depth]: + error = 'E127', 'over-indented for visual indent' + elif hang % 4: + error = 'E121', 'indentation is not a multiple of four' + else: + error = 'E126', 'over-indented for hanging indent' + yield start, "%s continuation line %s" % error + + # look for visual indenting + if parens[row] and token_type != tokenize.NL and not indent[depth]: + indent[depth] = start[1] + indent_chances[start[1]] = True + if verbose >= 4: + print(("bracket depth %s indent to %s" % (depth, start[1]))) + # deal with implicit string concatenation + elif token_type == tokenize.STRING or text in ('u', 'ur', 'b', 'br'): + indent_chances[start[1]] = str + + # keep track of bracket depth + if token_type == tokenize.OP: + if text in '([{': + depth += 1 + indent.append(0) + parens[row] += 1 + if verbose >= 4: + print(("bracket depth %s seen, col %s, visual min = %s" % + (depth, start[1], indent[depth]))) + elif text in ')]}' and depth > 0: + # parent indents should not be more than this one + prev_indent = indent.pop() or last_indent[1] + for d in range(depth): + if indent[d] > prev_indent: + indent[d] = 0 + for ind in list(indent_chances): + if ind >= prev_indent: + del indent_chances[ind] + depth -= 1 + if depth: + indent_chances[indent[depth]] = True + for idx in range(row, -1, -1): + if parens[idx]: + parens[idx] -= 1 + break + assert len(indent) == depth + 1 + if start[1] not in indent_chances: + # allow to line up tokens + indent_chances[start[1]] = text + + last_token_multiline = (start[0] != end[0]) + + if indent_next and rel_indent[-1] == 4: + yield (last_indent, "E125 continuation line does not distinguish " + "itself from next logical line") + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type = tokens[0][0] + prev_text = tokens[0][1] + prev_end = tokens[0][3] + for index in range(1, len(tokens)): + token_type, text, start, end, line = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + (prev_type == tokenize.NAME or prev_text in '}])') and + # Syntax "class A (B):" is allowed, but avoid it + (index < 2 or tokens[index - 2][1] != 'class') and + # Allow "return (a.foo for a in range(5))" + not keyword.iskeyword(prev_text)): + yield prev_end, "E211 whitespace before '%s'" % text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + for match in OPERATOR_REGEX.finditer(logical_line): + before, after = match.groups() + + if '\t' in before: + yield match.start(1), "E223 tab before operator" + elif len(before) > 1: + yield match.start(1), "E221 multiple spaces before operator" + + if '\t' in after: + yield match.start(2), "E224 tab after operator" + elif len(after) > 1: + yield match.start(2), "E222 multiple spaces after operator" + + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: baz(**kwargs) + Okay: negative = -1 + Okay: spam(-1) + Okay: alpha[:-i] + Okay: if not -5 < x < +5:\n pass + Okay: lambda *args, **kw: (args, kw) + + E225: i=i+1 + E225: submitted +=1 + E225: x = x*2 - 1 + E225: hypot2 = x*x + y*y + E225: c = (a+b) * (a-b) + E225: c = alpha -4 + E225: z = x **y + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3000 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start != prev_end: + need_space = False + elif text == '>' and prev_text in ('<', '-'): + # Tolerate the "<>" operator, even if running Python 3 + # Deal with Python 3's annotated return value "->" + pass + else: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + elif token_type == tokenize.OP and prev_end is not None: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in BINARY_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + if prev_type == tokenize.OP: + if prev_text in '}])': + need_space = True + elif prev_type == tokenize.NAME: + if prev_text not in KEYWORDS: + need_space = True + elif prev_type not in SKIP_TOKENS: + need_space = True + if need_space and start == prev_end: + yield prev_end, "E225 missing whitespace around operator" + need_space = False + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + r""" + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line): + found = m.start() + 1 + if '\t' in m.group(): + yield found, "E242 tab after '%s'" % m.group()[0] + else: + yield found, "E241 multiple spaces after '%s'" % m.group()[0] + + +def whitespace_around_named_parameter_equals(logical_line, tokens): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + no_space = False + prev_end = None + for token_type, text, start, end, line in tokens: + if no_space: + no_space = False + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + elif token_type == tokenize.OP: + if text == '(': + parens += 1 + elif text == ')': + parens -= 1 + elif parens and text == '=': + no_space = True + if start != prev_end: + yield (prev_end, + "E251 no spaces around keyword / parameter equals") + prev_end = end + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + yield (prev_end, + "E261 at least two spaces before inline comment") + if text.startswith('# ') or not text.startswith('# '): + yield start, "E262 inline comment should start with '# '" + elif token_type != tokenize.NL: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if -1 < found: + yield found, "E401 multiple imports on one line" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + """ + line = logical_line + found = line.find(':') + if -1 < found < len(line) - 1: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + before.count('(') <= before.count(')') and # (Python 3 annotation) + not LAMBDA_REGEX.search(before)): # lambda x: x + yield found, "E701 multiple statements on one line (colon)" + found = line.find(';') + if -1 < found: + yield found, "E702 multiple statements on one line (semicolon)" + + +def explicit_line_join(logical_line, tokens): + r""" + Avoid explicit line join between brackets. + + The preferred way of wrapping long lines is by using Python's implied line + continuation inside parentheses, brackets and braces. Long lines can be + broken over multiple lines by wrapping expressions in parentheses. These + should be used in preference to using a backslash for line continuation. + + E502: aaa = [123, \\n 123] + E502: aaa = ("bbb " \\n "ccc") + + Okay: aaa = [123,\n 123] + Okay: aaa = ("bbb "\n "ccc") + Okay: aaa = "bbb " \\n "ccc" + """ + prev_start = prev_end = parens = 0 + for token_type, text, start, end, line in tokens: + if start[0] != prev_start and parens and backslash: + yield backslash, "E502 the backslash is redundant between brackets" + if end[0] != prev_end: + if line.rstrip('\r\n').endswith('\\'): + backslash = (end[0], len(line.splitlines()[-1]) - 1) + else: + backslash = None + prev_start = prev_end = end[0] + else: + prev_start = start[0] + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in ')]}': + parens -= 1 + + +def comparison_to_singleton(logical_line): + """ + Comparisons to singletons like None should always be done + with "is" or "is not", never the equality operators. + + Okay: if arg is not None: + E711: if arg != None: + E712: if arg == True: + + Also, beware of writing if x when you really mean if x is not None -- + e.g. when testing whether a variable or argument that defaults to None was + set to some other value. The other value might have a type (such as a + container) that could be false in a boolean context! + """ + match = COMPARE_SINGLETON_REGEX.search(logical_line) + if match: + same = (match.group(1) == '==') + singleton = match.group(2) + msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) + if singleton in ('None',): + code = 'E711' + else: + code = 'E712' + nonzero = ((singleton == 'True' and same) or + (singleton == 'False' and not same)) + msg += " or 'if %scond:'" % ('' if nonzero else 'not ') + yield match.start(1), ("%s comparison to %s should be %s" % + (code, singleton, msg)) + + +def comparison_type(logical_line): + """ + Object type comparisons should always use isinstance() instead of + comparing types directly. + + Okay: if isinstance(obj, int): + E721: if type(obj) is type(1): + + When checking if an object is a string, keep in mind that it might be a + unicode string too! In Python 2.3, str and unicode have a common base + class, basestring, so you can do: + + Okay: if isinstance(obj, basestring): + Okay: if type(a1) is type(b1): + """ + match = COMPARE_TYPE_REGEX.search(logical_line) + if match: + inst = match.group(3) + if inst and isidentifier(inst) and inst not in SINGLETONS: + return # Allow comparison for types which are not obvious + yield match.start(1), "E721 do not compare types, use 'isinstance()'" + + +def python_3000_has_key(logical_line): + r""" + The {}.has_key() method will be removed in the future version of + Python. Use the 'in' operation instead. + + Okay: if "alph" in d:\n print d["alph"] + W601: assert d.has_key('alph') + """ + pos = logical_line.find('.has_key(') + if pos > -1: + yield pos, "W601 .has_key() is deprecated, use 'in'" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form will be removed in Python 3000. + + Okay: raise DummyError("Message") + W602: raise DummyError, "Message" + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match and not RERAISE_COMMA_REGEX.match(logical_line): + yield match.start(1), "W602 deprecated form of raising exception" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3000. + + Okay: if a != 'no': + W603: if a <> 'no': + """ + pos = logical_line.find('<>') + if pos > -1: + yield pos, "W603 '<>' is deprecated, use '!='" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3000. + Use repr() instead. + + Okay: val = repr(1 + 2) + W604: val = `1 + 2` + """ + pos = logical_line.find('`') + if pos > -1: + yield pos, "W604 backticks are deprecated, use 'repr()'" + + +############################################################################## +# Helper functions +############################################################################## + + +if '' == ''.encode(): + # Python 2: implicit encoding. + def readlines(filename): + f = open(filename) + try: + return f.readlines() + finally: + f.close() + + isidentifier = re.compile(r'[a-zA-Z_]\w*').match + stdin_get_value = sys.stdin.read +else: + # Python 3 + def readlines(filename): + f = open(filename, 'rb') + try: + coding, lines = tokenize.detect_encoding(f.readline) + f = TextIOWrapper(f, coding, line_buffering=True) + return [l.decode(coding) for l in lines] + f.readlines() + except (LookupError, SyntaxError, UnicodeError): + f.close() + # Fall back if files are improperly declared + f = open(filename, encoding='latin-1') + return f.readlines() + finally: + f.close() + + isidentifier = str.isidentifier + stdin_get_value = TextIOWrapper(sys.stdin.buffer, errors='ignore').read +readlines.__doc__ = " Read the source code." + + +def expand_indent(line): + r""" + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 16 + """ + if '\t' not in line: + return len(line) - len(line.lstrip()) + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + # String modifiers (e.g. u or r) + start = text.index(text[-1]) + 1 + end = len(text) - 1 + # Triple quotes + if text[-3:] in ('"""', "'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def parse_udiff(diff, patterns=None, parent='.'): + rv = {} + path = nrows = None + for line in diff.splitlines(): + if nrows: + if line[:1] != '-': + nrows -= 1 + continue + if line[:3] == '@@ ': + row, nrows = [int(g) for g in HUNK_REGEX.match(line).groups()] + rv[path].update(list(range(row, row + nrows))) + elif line[:3] == '+++': + path = line[4:].split('\t', 1)[0] + if path[:2] == 'b/': + path = path[2:] + rv[path] = set() + return dict([(os.path.join(parent, path), rows) + for (path, rows) in list(rv.items()) + if rows and filename_match(path, patterns)]) + + +def filename_match(filename, patterns, default=True): + """ + Check if patterns contains a pattern that matches filename. + If patterns is unspecified, this always returns True. + """ + if not patterns: + return default + return any(fnmatch(filename, pattern) for pattern in patterns) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +def find_checks(argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name. + """ + for name, function in list(globals().items()): + if not inspect.isfunction(function): + continue + args = inspect.getargspec(function)[0] + if args and args[0].startswith(argument_name): + codes = ERRORCODE_REGEX.findall(function.__doc__ or '') + yield name, codes, function, args + + +class Checker(object): + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename, lines=None, + options=None, report=None, **kwargs): + if options is None: + options = StyleGuide(kwargs).options + else: + assert not kwargs + self._io_error = None + self._physical_checks = options.physical_checks + self._logical_checks = options.logical_checks + self.max_line_length = options.max_line_length + self.verbose = options.verbose + self.filename = filename + if filename is None: + self.filename = 'stdin' + self.lines = lines or [] + elif lines is None: + try: + self.lines = readlines(filename) + except IOError: + exc_type, exc = sys.exc_info()[:2] + self._io_error = '%s: %s' % (exc_type.__name__, exc) + self.lines = [] + else: + self.lines = lines + self.report = report or options.report + self.report_error = self.report.error + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and line[:1] in WHITESPACE: + self.indent_char = line[0] + for name, check, argument_names in self._physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + self.report_error(self.line_number, offset, text, check) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type in SKIP_TOKENS: + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_row, end = previous[3] + start_row, start = token[2] + if end_row != start_row: # different row + prev_text = self.lines[end_row - 1][end - 1] + if prev_text == ',' or (prev_text not in '{[(' + and text not in '}])'): + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_row - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + assert self.logical_line.strip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + self.build_tokens_line() + self.report.increment_logical_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if self.verbose >= 2: + print((self.logical_line[:80].rstrip())) + for name, check, argument_names in self._logical_checks: + if self.verbose >= 4: + print((' ' + name)) + for result in self.run_check(check, argument_names): + offset, text = result + if isinstance(offset, tuple): + orig_number, orig_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + orig_number = token[2][0] + orig_offset = (token[2][1] + offset - token_offset) + self.report_error(orig_number, orig_offset, text, check) + self.previous_logical = self.logical_line + + def generate_tokens(self): + if self._io_error: + self.report_error(1, 0, 'E902 %s' % self._io_error, readlines) + tokengen = tokenize.generate_tokens(self.readline_check_physical) + try: + for token in tokengen: + yield token + except (SyntaxError, tokenize.TokenError): + exc_type, exc = sys.exc_info()[:2] + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + self.report_error(offset[0], offset[1], + 'E901 %s: %s' % (exc_type.__name__, exc.args[0]), + self.generate_tokens) + generate_tokens.__doc__ = " Check if the syntax is valid." + + def check_all(self, expected=None, line_offset=0): + """ + Run all checks on the input file. + """ + self.report.init_file(self.filename, self.lines, expected, line_offset) + self.line_number = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + parens = 0 + for token in self.generate_tokens(): + self.tokens.append(token) + token_type, text = token[0:2] + if self.verbose >= 3: + if token[2][0] == token[3][0]: + pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) + else: + pos = 'l.%s' % token[3][0] + print(('l.%s\t%s\t%s\t%r' % + (token[2][0], pos, tokenize.tok_name[token[0]], text))) + if token_type == tokenize.COMMENT or token_type == tokenize.STRING: + for sre in re.finditer(r"[:.;,] ?[A-Za-z]", text): + pos = sre.span()[0] + part = text[:pos] + line = token[2][0] + part.count('\n') + offset = 0 if part.count('\n') > 0 else token[2][1] + col = offset + pos - part.rfind('\n') + 1 + if sre.group(0)[0] == '.': + self.report_error(line, col, + 'E289 Too many spaces after period. Use only one.', + check=None) + elif sre.group(0)[0] == ',': + self.report_error(line, col, + 'E288 Too many spaces after comma. Use only one.', + check=None) + else: + self.report_error(line, col, + 'E287 Too many spaces after punctuation. ' + 'Use only one.', + check=None) + if token_type == tokenize.OP: + if text in '([{': + parens += 1 + elif text in '}])': + parens -= 1 + elif not parens: + if token_type == tokenize.NEWLINE: + if self.blank_lines < blank_lines_before_comment: + self.blank_lines = blank_lines_before_comment + self.check_logical() + self.tokens = [] + self.blank_lines = blank_lines_before_comment = 0 + elif token_type == tokenize.NL: + if len(self.tokens) == 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + elif token_type == tokenize.COMMENT and len(self.tokens) == 1: + if blank_lines_before_comment < self.blank_lines: + blank_lines_before_comment = self.blank_lines + self.blank_lines = 0 + if COMMENT_WITH_NL: + # The comment also ends a physical line + self.tokens = [] + if self.blank_lines > 1: + self.report_error(token[2][0],0, + 'E389 File ends in multiple blank lines', + check=None) + + return self.report.get_file_results() + + +class BaseReport(object): + """Collect the results of the checks.""" + print_filename = False + + def __init__(self, options): + self._benchmark_keys = options.benchmark_keys + self._ignore_code = options.ignore_code + # Results + self.elapsed = 0 + self.total_errors = 0 + self.counters = dict.fromkeys(self._benchmark_keys, 0) + self.messages = {} + + def start(self): + """Start the timer.""" + self._start_time = time.time() + + def stop(self): + """Stop the timer.""" + self.elapsed = time.time() - self._start_time + + def init_file(self, filename, lines, expected, line_offset): + """Signal a new file.""" + self.filename = filename + self.lines = lines + self.expected = expected or () + self.line_offset = line_offset + self.file_errors = 0 + self.counters['files'] += 1 + self.counters['physical lines'] += len(lines) + + def increment_logical_line(self): + """Signal a new logical line.""" + self.counters['logical lines'] += 1 + + def error(self, line_number, offset, text, check): + """Report an error, according to options.""" + code = text[:4] + if self._ignore_code(code): + return + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + self.messages[code] = text[5:] + # Don't care about expected errors or warnings + if code in self.expected: + return + if self.print_filename and not self.file_errors: + print((self.filename)) + self.file_errors += 1 + self.total_errors += 1 + return code + + def get_file_results(self): + """Return the count of errors and warnings for this file.""" + return self.file_errors + + def get_count(self, prefix=''): + """Return the total count of errors and warnings.""" + return sum([self.counters[key] + for key in self.messages if key.startswith(prefix)]) + + def get_statistics(self, prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + return ['%-7s %s %s' % (self.counters[key], key, self.messages[key]) + for key in sorted(self.messages) if key.startswith(prefix)] + + def print_statistics(self, prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in self.get_statistics(prefix): + print(line) + + def print_benchmark(self): + """Print benchmark numbers.""" + print(('%-7.2f %s' % (self.elapsed, 'seconds elapsed'))) + if self.elapsed: + for key in self._benchmark_keys: + print(('%-7d %s per second (%d total)' % + (self.counters[key] / self.elapsed, key, + self.counters[key]))) + + +class FileReport(BaseReport): + print_filename = True + + +class StandardReport(BaseReport): + """Collect and print the results of the checks.""" + + def __init__(self, options): + super(StandardReport, self).__init__(options) + self._fmt = REPORT_FORMAT.get(options.format.lower(), + options.format) + self._repeat = options.repeat + self._show_source = options.show_source + self._show_pep8 = options.show_pep8 + + def error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + code = super(StandardReport, self).error(line_number, offset, + text, check) + if code and (self.counters[code] == 1 or self._repeat): + print((self._fmt % { + 'path': self.filename, + 'row': self.line_offset + line_number, 'col': offset + 1, + 'code': code, 'text': text[5:], + })) + if self._show_source: + if line_number > len(self.lines): + line = '' + else: + line = self.lines[line_number - 1] + print((line.rstrip())) + print((' ' * offset + '^')) + if self._show_pep8 and check is not None: + print((check.__doc__.lstrip('\n').rstrip())) + return code + + +class DiffReport(StandardReport): + """Collect and print the results for the changed lines only.""" + + def __init__(self, options): + super(DiffReport, self).__init__(options) + self._selected = options.selected_lines + + def error(self, line_number, offset, text, check): + if line_number not in self._selected[self.filename]: + return + return super(DiffReport, self).error(line_number, offset, text, check) + + +class TestReport(StandardReport): + """Collect the results for the tests.""" + + def __init__(self, options): + options.benchmark_keys += ['test cases', 'failed tests'] + super(TestReport, self).__init__(options) + self._verbose = options.verbose + + def get_file_results(self): + # Check if the expected errors were found + label = '%s:%s:1' % (self.filename, self.line_offset) + codes = sorted(self.expected) + for code in codes: + if not self.counters.get(code): + self.file_errors += 1 + self.total_errors += 1 + print(('%s: error %s not found' % (label, code))) + if self._verbose and not self.file_errors: + print(('%s: passed (%s)' % + (label, ' '.join(codes) or 'Okay'))) + self.counters['test cases'] += 1 + if self.file_errors: + self.counters['failed tests'] += 1 + # Reset counters + for key in set(self.counters) - set(self._benchmark_keys): + del self.counters[key] + self.messages = {} + return self.file_errors + + def print_results(self): + results = ("%(physical lines)d lines tested: %(files)d files, " + "%(test cases)d test cases%%s." % self.counters) + if self.total_errors: + print((results % ", %s failures" % self.total_errors)) + else: + print((results % "")) + print(("Test failed." if self.total_errors else "Test passed.")) + + +class StyleGuide(object): + """Initialize a PEP-8 instance with few options.""" + + def __init__(self, *args, **kwargs): + # build options from the command line + parse_argv = kwargs.pop('parse_argv', False) + config_file = kwargs.pop('config_file', None) + options, self.paths = process_options(parse_argv=parse_argv, + config_file=config_file) + if args or kwargs: + # build options from dict + options_dict = dict(*args, **kwargs) + options.__dict__.update(options_dict) + if 'paths' in options_dict: + self.paths = options_dict['paths'] + + self.runner = self.input_file + self.options = options + + if not options.reporter: + options.reporter = BaseReport if options.quiet else StandardReport + + for index, value in enumerate(options.exclude): + options.exclude[index] = value.rstrip('/') + # Ignore all checks which are not explicitly selected + options.select = tuple(options.select or ()) + options.ignore = tuple(options.ignore or options.select and ('',)) + options.benchmark_keys = BENCHMARK_KEYS[:] + options.ignore_code = self.ignore_code + options.physical_checks = self.get_checks('physical_line') + options.logical_checks = self.get_checks('logical_line') + self.init_report() + + def init_report(self, reporter=None): + """Initialize the report instance.""" + self.options.report = (reporter or self.options.reporter)(self.options) + return self.options.report + + def check_files(self, paths=None): + """Run all checks on the paths.""" + if paths is None: + paths = self.paths + report = self.options.report + runner = self.runner + report.start() + for path in paths: + if os.path.isdir(path): + self.input_dir(path) + elif not self.excluded(path): + runner(path) + report.stop() + return report + + def input_file(self, filename, lines=None, expected=None, line_offset=0): + """Run all checks on a Python source file.""" + if self.options.verbose: + print(('checking %s' % filename)) + fchecker = Checker(filename, lines=lines, options=self.options) + return fchecker.check_all(expected=expected, line_offset=line_offset) + + def input_dir(self, dirname): + """Check all files in this directory and all subdirectories.""" + dirname = dirname.rstrip('/') + if self.excluded(dirname): + return 0 + counters = self.options.report.counters + verbose = self.options.verbose + filepatterns = self.options.filename + runner = self.runner + for root, dirs, files in os.walk(dirname): + if verbose: + print(('directory ' + root)) + counters['directories'] += 1 + for subdir in sorted(dirs): + if self.excluded(subdir): + dirs.remove(subdir) + for filename in sorted(files): + # contain a pattern that matches? + if ((filename_match(filename, filepatterns) and + not self.excluded(filename))): + runner(os.path.join(root, filename)) + + def excluded(self, filename): + """ + Check if options.exclude contains a pattern that matches filename. + """ + basename = os.path.basename(filename) + return filename_match(basename, self.options.exclude, default=False) + + def ignore_code(self, code): + """ + Check if the error code should be ignored. + + If 'options.select' contains a prefix of the error code, + return False. Else, if 'options.ignore' contains a prefix of + the error code, return True. + """ + return (code.startswith(self.options.ignore) and + not code.startswith(self.options.select)) + + def get_checks(self, argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name and which contain selected tests. + """ + checks = [] + for name, codes, function, args in find_checks(argument_name): + if any(not (code and self.ignore_code(code)) for code in codes): + checks.append((name, function, args)) + return sorted(checks) + + +def init_tests(pep8style): + """ + Initialize testing framework. + + A test file can provide many tests. Each test starts with a + declaration. This declaration is a single line starting with '#:'. + It declares codes of expected failures, separated by spaces or 'Okay' + if no failure is expected. + If the file does not contain such declaration, it should pass all + tests. If the declaration is empty, following lines are not checked, + until next declaration. + + Examples: + + * Only E224 and W701 are expected: #: E224 W701 + * Following example is conform: #: Okay + * Don't check these lines: #: + """ + report = pep8style.init_report(TestReport) + runner = pep8style.input_file + + def run_tests(filename): + """Run all the tests from a file.""" + lines = readlines(filename) + ['#:\n'] + line_offset = 0 + codes = ['Okay'] + testcase = [] + count_files = report.counters['files'] + for index, line in enumerate(lines): + if not line.startswith('#:'): + if codes: + # Collect the lines of the test case + testcase.append(line) + continue + if codes and index: + codes = [c for c in codes if c != 'Okay'] + # Run the checker + runner(filename, testcase, expected=codes, + line_offset=line_offset) + # output the real line numbers + line_offset = index + 1 + # configure the expected errors + codes = line.split()[1:] + # empty the test case buffer + del testcase[:] + report.counters['files'] = count_files + 1 + return report.counters['failed tests'] + + pep8style.runner = run_tests + + +def selftest(options): + """ + Test all check functions with test cases in docstrings. + """ + count_failed = count_all = 0 + report = BaseReport(options) + counters = report.counters + checks = options.physical_checks + options.logical_checks + for name, check, argument_names in checks: + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match is None: + continue + code, source = match.groups() + checker = Checker(None, options=options, report=report) + for part in source.split(r'\n'): + part = part.replace(r'\t', '\t') + part = part.replace(r'\s', ' ') + checker.lines.append(part + '\n') + checker.check_all() + error = None + if code == 'Okay': + if len(counters) > len(options.benchmark_keys): + codes = [key for key in counters + if key not in options.benchmark_keys] + error = "incorrectly found %s" % ', '.join(codes) + elif not counters.get(code): + error = "failed to find %s" % code + # Keep showing errors for multiple tests + for key in set(counters) - set(options.benchmark_keys): + del counters[key] + report.messages = {} + count_all += 1 + if not error: + if options.verbose: + print(("%s: %s" % (code, source))) + else: + count_failed += 1 + print(("%s: %s:" % (__file__, error))) + for line in checker.lines: + print((line.rstrip())) + return count_failed, count_all + + +def read_config(options, args, arglist, parser): + """Read both user configuration and local configuration.""" + config = RawConfigParser() + + user_conf = options.config + if user_conf and os.path.isfile(user_conf): + if options.verbose: + print(('user configuration: %s' % user_conf)) + config.read(user_conf) + + parent = tail = args and os.path.abspath(os.path.commonprefix(args)) + while tail: + local_conf = os.path.join(parent, '.pep8') + if os.path.isfile(local_conf): + if options.verbose: + print(('local configuration: %s' % local_conf)) + config.read(local_conf) + break + parent, tail = os.path.split(parent) + + if config.has_section('pep8'): + option_list = dict([(o.dest, o.type or o.action) + for o in parser.option_list]) + + # First, read the default values + new_options, _ = parser.parse_args([]) + + # Second, parse the configuration + for opt in config.options('pep8'): + if options.verbose > 1: + print((' %s = %s' % (opt, config.get('pep8', opt)))) + if opt.replace('_', '-') not in parser.config_options: + print(('Unknown option: \'%s\'\n not in [%s]' % + (opt, ' '.join(parser.config_options)))) + sys.exit(1) + normalized_opt = opt.replace('-', '_') + opt_type = option_list[normalized_opt] + if opt_type in ('int', 'count'): + value = config.getint('pep8', opt) + elif opt_type == 'string': + value = config.get('pep8', opt) + else: + assert opt_type in ('store_true', 'store_false') + value = config.getboolean('pep8', opt) + setattr(new_options, normalized_opt, value) + + # Third, overwrite with the command-line options + options, _ = parser.parse_args(arglist, values=new_options) + + return options + + +def process_options(arglist=None, parse_argv=False, config_file=None): + """Process options passed either via arglist or via command line args.""" + if not arglist and not parse_argv: + # Don't read the command line if the module is used as a library. + arglist = [] + if config_file is True: + config_file = DEFAULT_CONFIG + parser = OptionParser(version=__version__, + usage="%prog [options] input ...") + parser.config_options = [ + 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count', + 'format', 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose'] + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', default=True, action='store_true', + help="(obsolete) show all occurrences of the same error") + parser.add_option('--first', action='store_false', dest='repeat', + help="show first occurrence of each error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %default)") + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns " + "(default: %default)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error " + "(implies --first)") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--max-line-length', type='int', metavar='n', + default=MAX_LINE_LENGTH, + help="set maximum allowed line length " + "(default: %default)") + parser.add_option('--format', metavar='format', default='default', + help="set the error format [default|pylint|<custom>]") + parser.add_option('--diff', action='store_true', + help="report only lines changed according to the " + "unified diff received on STDIN") + group = parser.add_option_group("Testing Options") + group.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + group.add_option('--doctest', action='store_true', + help="run doctest on myself") + group.add_option('--benchmark', action='store_true', + help="measure processing speed") + group = parser.add_option_group("Configuration", description=( + "The project options are read from the [pep8] section of the .pep8 " + "file located in any parent folder of the path(s) being processed. " + "Allowed options are: %s." % ', '.join(parser.config_options))) + group.add_option('--config', metavar='path', default=config_file, + help="config file location (default: %default)") + + options, args = parser.parse_args(arglist) + options.reporter = None + + if options.testsuite: + args.append(options.testsuite) + elif not options.doctest: + if parse_argv and not args: + if os.path.exists('.pep8') or options.diff: + args = ['.'] + else: + parser.error('input not specified') + options = read_config(options, args, arglist, parser) + options.reporter = parse_argv and options.quiet == 1 and FileReport + + if options.filename: + options.filename = options.filename.split(',') + options.exclude = options.exclude.split(',') + if options.select: + options.select = options.select.split(',') + if options.ignore: + options.ignore = options.ignore.split(',') + elif not (options.select or + options.testsuite or options.doctest) and DEFAULT_IGNORE: + # The default choice: ignore controversial checks + # (for doctest and testsuite, all checks are required) + options.ignore = DEFAULT_IGNORE.split(',') + + if options.diff: + options.reporter = DiffReport + stdin = stdin_get_value() + options.selected_lines = parse_udiff(stdin, options.filename, args[0]) + args = sorted(options.selected_lines) + + return options, args + + +def _main(): + """Parse options and run checks on Python source.""" + pep8style = StyleGuide(parse_argv=True, config_file=True) + options = pep8style.options + if options.doctest: + import doctest + fail_d, done_d = doctest.testmod(report=False, verbose=options.verbose) + fail_s, done_s = selftest(options) + count_failed = fail_s + fail_d + if not options.quiet: + count_passed = done_d + done_s - count_failed + print(("%d passed and %d failed." % (count_passed, count_failed))) + print(("Test failed." if count_failed else "Test passed.")) + if count_failed: + sys.exit(1) + if options.testsuite: + init_tests(pep8style) + report = pep8style.check_files() + if options.statistics: + report.print_statistics() + if options.benchmark: + report.print_benchmark() + if options.testsuite and not options.quiet: + report.print_results() + if report.total_errors: + if options.count: + sys.stderr.write(str(report.total_errors) + '\n') + sys.exit(1) + + +if __name__ == '__main__': + _main() diff --git a/external/plyer/tools/pep8checker/pep8base.html b/external/plyer/tools/pep8checker/pep8base.html new file mode 100644 index 0000000..e69ca6f --- /dev/null +++ b/external/plyer/tools/pep8checker/pep8base.html @@ -0,0 +1,70 @@ +<html> + <head> + <title>Kivy Styleguide Check</title> + <style type="text/css"> + body, html + { + background: black url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQMAAAGQCAMAAACd/fUUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAxQTFRFJysvIyMjJSgqJCYnQLTKlAAAMgtJREFUeNrMXYty5DgOk4H//+ebbtsS36Ld2apL7d3OThK3DVMUCYLUGJ8vHgfG+joOqv/62y/8xbXnRZj+COdn/Puh+VGQnywfE//+e/2XQYT1fXx+l/j3df0c/33V98+/wJcdDPi9P45//+D6W/3JQ78PymceBqDqVcg/sfdoe3zbpoACAxynGYx/P8sQA9jnxPzTeIIBj2H/1Dfl+l3j83PTwh5igO/94N+/sZYCDfrpgyJHgOeXwoDrZ9Mno1wu4rfR8xuMvw+WxvZdDuf/YC8Kj4G4HMPPZmw24/yk0g4YY4qtlX1Q4xe9pudQnui7BMbnOsFSiBbDCcPnA0NYzI/qlSUMO3i5HPZjNDgWhAkwrWHvAPhn9PKl/buf+778Uph/M6ovhE5MWMewdoAAA0aX1mvEPcv3keE83BYAHoe4tngS+KUQOoSRrIwABNoVvf7ePiPiy2f2z/lrfokAOwC0uayP5vpAfxPomICHVb1isY8cDoMU49QJnM8S/kC2GgjpyxBZIOQN3Y9dGELsa6O4RkZD158JGSEVKy3dDD5vkuH3Y0Pg936xfF7kiylsg2bFNgFQEPTDmra3iaKbyEguZ2E2gvNR52MzDJeFGcw7Y7AYwMZWjVd5QQuEFW6v7+L+5/SK1LcJE///g8hcGOq1zc8VILADQLyy2gF99sXCt4kF/LnfrwV8MdCB1pCO78IAwyGrXATlzQk8/PN/7Us9DM9/H39kBuJSOkjAitg/r/mMuxDujhy0L4flHuoe6nLoUZKn41heMFNcgfzJG/gtFjpmGOtbWIlumE6YrY6Fw/J7VnLdf1bhQynpcyFCInxumHxjBiaFZ7QPDvUfOIoYvIEBNdYT3BHcOM3in66qWubXOmXbDK7HRuhorjV8XvN2Y/Qr3ZkVN3xH9M0RmO9R7OLsmngDA+QY3O8eE9cgbeIIkrEisRgPMLhszOTtc3G6h2YAAn/E4DJpiE/3jnswwqDcqI4uBv8AhvV8gfGbCFlZT8Md6PQzeEBxewy35eH2dhS7N3SSgAoDCJdbePnFO+EnDEwYIy2dnKsckZFPhoHXbhJFOd8fuR8B0VZkMYB2t8i9/NprokcbfQx8BGx9GC/C073ic1/68AaId6e1JkXcYbeiYbxxyJZEj4SFWbXMexhoUkU6AdS+HsgePnbh4VY0VuQd3jr3GCBKBN5gwBHleT/xz+EnJhiY77oQNMSAMhM194/uvmA3Hshn5o91CD7AQKdTwbsKHwCquLBFbZM1QVGS5j2+tIP43uxWNCQER+277TfkPb+JkaLbPTd4DNgaxsZ/9pYC44z6dER5gJsu74sURvJkeIjBd4s/n58r2vMYYJu+I+eIjhDKL/e+xyA155Q557OlgIuc5XHV7BBnVNhyGMhvO/nFcSXsZ3SRva30pSJ/uY/N4HKyV5k0XroM9xNmtVi0SMyhokgktzq6X+gTCPQYLMu3DuHw1EdCZSinzx1ztTDgSPdGX4YW91k/F59sjBIDikSZMi8GDhPC0wXHSNdwuoSGhCC8bWb7hfWI6e104iPhX6kS5UXgc1Z1qYsyzDFItSQ0GIyoHKcwYMMj8hGXxDA34CRCkMQ7FLnThQDC/TCNPVc6cL+0ISHIAwQ+3Ol2y4GqtGZ2bIZXcwYNxvGC3vxpfi8oJ4xjVx9sbnV4wi7Tb/XzliSlnb/f6/VnJSFDvUKWr/mlh24i7rsjg+WLU5cCnteNUlj1Xl8XMT4Zrf5dTTN1Y2co5GlLsiEI6FHmpR7E5uP2FtwyTQoDiCH4ltt6ATSGXWfMvVPxZMHzd27glJNoOY5dqszIgOvH1kYoIDhLrl9GaXcnH6YledzoftMiCi2d+NQco2v1tHzUpLN0CGMqaDZ8SmJjz55CujUC+EVcdzSL2a56TMOAcVWdx9GrMmnzeJScPyZ2WEk2+7V4xW3CVp3wKY2BknP8fq4KoZgmDGZv7Jgv9kJURhsISoFPiapyh8OVEc5gTVTkZtrJJWlzhiQwuPHhxrmTDQqD3PAGCoi+LEOqquA4ZonBSbirt+bMwHAQY5ZwaO9QgsKW70IvuWQWOvQMcZkBqXQI31rDBQ+GwQCS2tCveyyE1quCL7FgS+GgnWCnGJi8M1QvDiHSmCtd+TtQhppYGeStUIChHa6cieFDiKD7a+Vfy8j2Am/0p8a0iClRpVKlTNeoQA8VZnzRgwbtWAG6C6mHMBOkN8SpSL1i5hIDG6sws3kW+JQrxFx/HFMEwXHL9Q9VNRC88RVarPBySDECMhflMtBgZ6iCyxXlcIOBEr80ozCsD+WZdcG5xEPpfe6kK8BARIHcVIA8CuUOR0VvVckH/TIx4UYRu9qgSZGzBoPvDgk6DNInMT0uV2ne/8ix1z9Aa+WS5CtcOkV4bsDBOIwkwxfbv5wdlk8cCQPhvJeKFhi+6QcYIN0aQwaDRmZbhuFU7kBh8C3pf98jPJ84HyYDgalvYn1vkHJ37DFAVOWhlIdvQdAhtX5GSPnUsBhldQz5jAxA2KgXp3mlxEnaLTBDnIEnRdhrWxbltWx/iTBI3qnd8fKQPt8ZrBixCJGsvKnZbOdYk0MsJftdFR8kHG7MR3GX1mzZVWvyyEEQwjy80gwf0WJQqdMIzKBiFljx6HiMAU1Pqec2xS4/fsIAqTcbsewte1hegQ7+CIPDytMQ+lHa3ts39FXBHzC7YFwSQxCw/4ABrGDB9EKJaA8/YoAMhIpDQbRjUSSSrzGgX5TRctAYpHuITMwqei+7w8rHMkidZs46+DcYuJAAkfWK/tERM/Idsj/WgpbbTCQpOG4pllW8/46BrbObQmgg0BbMNFo4RGGevBys0BGBUBOL38R4ggE63OFQ6sRJbKQYzj6eu6Fl/sVVEXMLI958fPafZvBSguVcAhpxYswkqYfOFzCiWt0lwIVWtfEQ6ohAeT6g7cjD7LNXmewITfNgH4NCH1aII0ziDPVMuBusCbGJ3W47WxX0ybrS3jERlklGTsXwzzFQnRS7OQ7lJbnaNe8Vylls+YQwd2EZxthcC/pQyT8KDCjo58B067xRkDOBqWGbcGdxn9YFnevzq/C7hc7LjWvKHIKCzzE4vNLZPxka/EFQc3iAQbVQrirZuF36yk+/MzAEI4SqOj5UTBgznjh8cslrb3B6D93+mwgWjt8xuB72NGauqgHOh/2WqWX7Y8HxDBUo6J+j4TCCpYoHesQgVnbkFNUsgFJU+AkY7urBvRF+DePjHb606pxKAMwQixsMgED8R5UpKA2tdglvMDgQKPXZxOBzc9bEcaehV1OsF1CzxqBy0hzDZnnQiUO/zmTmZDh2Xi15pE4WYhrNqe+YgSbH9ea/FTOwrtn4wgJzeg6rNVREFvwNg6PEYBUJr8EoVAaEkXZWfGC4BxJ8AmDQxeKfK5JTk8Up1CrMQKwAz+WzjwHrGOKavcNK03b9oNGJXcE+hpM2CDVlyOjiQorc1L4hequXnwSeOoS4PoAHRsT4aXiFQKTvN4H9eENpYKcwW/GRFWSsNBo/LIWnDhUioLm2RPVWRdP2lVGQ82fC3i1d0QgKblwzFo41gGQZWrgYcH9J045FLHyKwRiuiUZpiukShq8HJUbW4KH1kWEwgyBLXo18rPi3RjT9GoNrkhQ08Euud++fZ+QIcIcBVkDvsmYxtEj6maGI+k3W8HcYcOUJtzvRngamsHvNKRjI+tRmJ4vUHZpCuAwNVwYBt8lXvfd2FSypxEs7wOH6K3HG57c143r138YALfKjEt7PJDPDANPsAsLoFjS84bo3vQE7n3hNxaPdBuYmg8knfSxAB/+THP1ax+0p6DqUIAKkGR/d5UrZaf4cAyVpeIzBSpqxdolbgs1LX7YynesHlE88N7mPgGURkndR47IgqgoHo8IsRMSIl1YwRtg2vt9Zl+YGOjw8TloJizQQCbDwV1QbwFzit3+cUxyi+RpUm8P892sziPqFGxhcRkuuQu7gtA1c7o3nvyH0xWo8EEyIBBkr4FBNrq72I+i7c894UfQxbcMPA+3ZfIC7OAoVNZw5k974lNBZA8s0XhIYnGO4ZJeXLEMj0MbcyRzvYFgESAiSnL0JKPJ/vkiuYRQiKpabPMTWT4QYHHWIBNPkHuyBqIJfBqVC7jvntNJxT1afzltNsNBtd1+aEUGJ6tb1TdlGtDWubSEqOTOuwEr22ltz3UWKXRIXfsxhe3DtD3xjBGOtDCZrMahoIRlMQ6DR8cMjzfxWuD1D21du5bvD3ZN0V+KjA4evhp3IONXo0dbbHV6Kt9hSYF9drNoFX1TRGRYpp8F/R+l+Y+Ornf5QCWQLgwhtqEx/ZYOdyprzeBJHdqLq+NK6+kTdg3ut2kW436zL8Q4DWbzjwwCowCDTkxXmgZ72ezZbeTE632JwcDfQ+REGbHk5bqmWzPCybZfxPNI/n6ofyQ6bGFg2jTu2KZg0YPpQcYf0CEUT/38Y7Ei3YuDEufrDQSR3ywWzsXn/HQY3g8YgAmLDkYr4KGdcqZQLHNFI37V9s6y1/TkIGYlybqnsmUGRNNG3DR9XaABk6wbjeIQB0PaBl4onfODFqD7KorZJE31HzMrjPsyGn1Uz6nkoV4FlscBP+njPBzTN40xznsZeUrTXD9N7JZJI0n1XZwupyY+yIw9PfACb77Qe5I3OBWDV35RcwKMh7yZT/gUE0hftNxSAbpc2pldkkSpBEgU/3I/P3ZOI8z06K7DnDkpFfnY/iB56Z0mivKXHh1xFv1PVi6JzMrV7sIPBNd5Qdw7dZU6MfrUgkGR6R5/9JuTeAIEBefn+UQxRr84a2GJAxN2fcwDu6Ht4jwGbGGBSg4SGENe8EK4xSu6FFa/naGCAm24hjMTwXtXob/SpQBIdW4Iki5RkRZFh4IN6FvLvQheeA4GrD4zac4TkUT3wG351u27WvOIBgUKhvg86kkx/jIJRuPQcDwQprjfMz7bsxQeqWIfd4VfoFa9ibyot1ukzuYq7jzBQwknlvzZyDJ08qOw+R4Ako/B56w5gnjfoqb+nzq3Db7o1Aoy0EP5K41b0weZv2Oh1Cgd+eAzWbAVeFUg+xEA9QxIPYr9muWXhsPcBqLdj2bKi9ZXXdrD8WruO3sHAXg0NIuVZPkodl6B0FaGQHJpnDXV+P6wFeMV7smO8heAJBlYwCN6RtjU1PsKAPkxMpayjGBjwOiWnLOKnYK1WDbhGlBuDK8r8CD7aU8b96v0BAxqZlFb9jC4GLLfiI5xgIwbO3AVddj28zxvzVb6NnugyMPpKTrrryXdau4ORY2ADxcdSktoO6GOVUZTuKNWLG6vUGNSu68oKsBSGPNzMUz7YGofPFIvwLK1XxwKlQ/UbtRVurNP8WF0Ha218hUFqsu0jTRwG6GIA0S+AKkiE2RfEC5Kvc67tv8Ig5cTKBMJigM19zPC7TBlpXDIPJW1zQ57bkqI4FHrxBYNom5hctFvM4kK3LqymikAJ+g4DNVfwZwwWtcbxN4mI6YoM2Qg8DkqK8PYXDIaXzI1Enf4OgSM+8CePz9hbwH0MlpIr63EIX8JvFhY9cDA05nmAXkS37XshdtcbzzHw7iCWF7laAF4bQhcDNIuz4aU6r4RTuIr9cuXcAIFtssLn7uBoLpnYFsJP79zMkZ4xRVG8KR/sJQi9BdSYMlth0CAmZ4yEXNkAN9hyjuzc3SYemsHxDIKjGC4YHMD5/EsNaKhHTeN4YQqG6YxBQ1TJT+ol1bbAH3eFahReNKhc3ybr8i91y2UlRWI6ICsauTp+x4Duck0QonY4pAiPqEgS3gSau8WLbaGzVstpheabl6tAUOdewoCoNB2KvBuseBODZ4aAdEZqWbWhiSXZ4XpLDNhwrbuJG3iu8UnuGyVCUDS9mgbRmAM7sn2crVeILgZPQNjGvSjMDvegVdb7SY6Bn0j14KBwoSALak38wQqO7THnItqSEXTDEHROtP7qiQ0jit0FKQKngUbVWg6rNVH5YS6uW7SFJICLGDKZkXJ9NJ9ZMALq+cYgP1oCLlvS2kI/TNpvVGbs6Sy83bQM4il96YCYEDE8sV5BpM1sIR/mYGqYDIaemaM0h5eHx4uCd/2fHoPtbIO3gk1YWheaJIsG5ftCmv9kHRsxcNTJFiEbv9B//gADPgzp1nXsdZnmlYKK9OCbozSHF8zFB4lhHRCxJIivNqXsaDTk6oCEig7ifR94D0MX+aM06X1PfLoNBASVxAlbDHbVgLTK5K/rXaN3J8PShu4ozWO0ZnvlZY8oFC1P4ECKAbeGYL95/SZdMi12Oju1MjhKky6g3VEGCbWPGrP4W6tZK1FRNupREIPgArduMQiO0jxGdiCurq8wKIaHRZvgSfNEUW//dXUhx0BmqO4IjQQDeZSmnUlv3T1gdPyxZ5P3yiJTDCxkXSH3p3U2WOZZOhKIj9IMuHU0Im2GvuucrGDVGvmgS245gPZkIlaKc0md+aM0/Vm6aF3f854cU8xgQMjTPGyfj8/UsdGrPGALdtr8EVJsjaTLBSPEdXaeLRYPd5hl7BDKgPldIAZwJn0rcTNHaUZsc9PSXH3ccictDLh7xe3CYpAWzOIxA3NCluc/yHus5ENv8Ec0LjiysrgsDjv++dFpqzN8/CSMZzQDsz6iLH7rerDBALBTD3oYZEbH0a+mxIT5JbAdeod0R2liG4ss5LjBQL/RBINehiFbR7sYOLKPlPEB9kOBU3kPQ/7KPs4RHb4+XmMwhKCqO8hTfEQUI7FRNctlQ1kMjeiORxz58ikG1AHXfhUhbkNcq2A/Kb3S4WKPAQPyApmh9sgTx2a0IyRjsRaD4IxWMfvgWSCGJOYN/vMpBuiIeNNtyxjC16WJKSG0NfcnEhMWQRJd8h3PtOB+XzQtFz1XojPFIIU1DgH8ewxmt8PN4WbnU6VuM+FNmmy8ubOo+wWqPIj/AIPB6ptJJZL72LjHQtp02W8mmCNRTzN4hQFo6feioJnrDeA9f3SRaLRQP1e4zoPVHut2evVZ8vGJxfQnmCC5J9UmWgRxq+6SwbhnHuoY1qchZtyXn/3VUoRZ+v1dxXddWwOG0I56VYngPv2EEmp9SABbr48Gked+hgEXlY5t5NbrZo5EofRMxZjNNRwIop2U4PgZAzd3aq8zQObuSzO4Z6DN0NiR0KcjuFoVmWDAP8UgmbrFjTIEqR32mCYmhnCUDmEkHRqopTD5w9czx1DoY1idEYAW0XTzy/aN3mEDkBR+0PMHipIL95Cj8RVRJkHVHj1FYMyEHPBzHWffMvxYxTRvTBLu49evt7qRwiG6v3CGMFMGHt4fUGQ5iMb3vX5oNUTpXSfITteuqDZ99ok1hInB3cJtMWB0ILgo1j6fgZkHgR0eASW08mQSWdzRXIV1vyOTn56RpdYV2MnlKNz4My0vWpQI+G51aQxIdfIE7uHj4eE9OK7BzUr6OAehDGmFfIoAzMEj2JlCfKR8bYUUEga1/c17KIaJHXcVXlkW1YnlOuF5NhotIk/K/ITFqtqOILVPeh5txkLBpSLl71xvKM3OZGU4K6+mPht6CcSLGnElAGG+4DRUYIn/pUihOU7lmLX93eoSb5wecw5VkoQ+teGefrOifz/MBMgMoWNSStiRmvKlFLMYbI4nP+Io1Qm5YVQ/87gOe15gUs2i5woebK7m7NNs0O81QCrsSSwJ5RgWx1zAHEFyTqjxg0AZMq0MKFa+wwDnqQtB+ZfXOSQPMLgGKiabjFk6U7UwI+H5hqMzel3SPvy0Fz7Z83IlzHpxc8xeE4M1BJWhddB7zEn5k2fd2i/PSjDCvTy8gQFTTQcVBtxiQKZPS04piZLc0B2wR7fbcDf9ZYy3dmDYJsIVHXBvnziicZrjpo7gWGe4rhaoB6TcGs3sMNcMhXQ+QpRi0nKTcrSa3m7N6FEY7RCn7LTAAJqWGms7QHgAi3gQte2AVt2A+Fz3UJpYZrvyT7QbB5Lqi7pTzBjWezmu3ofvHynPJ6Uj+eBEHL5wxXvWtQvXi8ICSgwoSOStKCbEYASKQ82RqekekJok43FXpd+gs2ghIvYm4dnZTXWNOEnKnn4aZhNIC7Ha0V+uG0N2H5mA0zWHIVolTKWWyQyFtnBIHCaR2EFQv0gLsacUW9/HhcExdzWaN3k4uBEVbjldFvNdFGabNDxZfBK6Fl7f79HwIurgBbi5rJ+7WMdPyfeKxY2JGU/2KGhHJofzQaptC+Eeg1pl6ielMXlEzCB75VVZ6e47el74OBmbHTKSp7pdV+6C2XQbLEGIWYmBu33I/l27FO77RC68mVeBEWHP4Gx5AEJ4dq4sE8nttSxhpkUZhYI8m6atVBxbDEAi0bldFQVVsuUd/E8MJCdxctmy/Obz+k5KE7c8+Nay8MdkNMT7jF6qQXOX/NmFWyEG7oQG3OtUDcGsmah33HFw9Az+hCVESkRGZWwO4Q60EV7vmVum4i0GobonOEbml/MPjYkGMnicjCj9Zu871UJu8gpTjz/8wnhDmbTKzyFOV35rQ48rX+CWm3QEFPl7KSmizvjUGNjB4O5NckthGJVuxU0iEkbjZxRcUIQXl8XmZngG2F93CB9+SQxKbjJplPi79eD2bOdTU3+xw+A6w+3r/X2eoQ50LLnJuyV4qz7YlxeBSFKDHANsqJXd4VkXd3T+z6nRkGKQcZOdBqF9qBAFg6Ju4Yns64yYlOHccW1XABEsBdPv3OAmMd6DwEKIJgUyULUru2fj8fbM1QN9RCl3gkHCTYbJOu3KjZctdwJiH6LxiHrWH+/PKhwNVP7UGNTcpB5/gii2S+O9RwflHEjIPFGSehOGMOaWNAYlNxkfm7EKx1b70JJOpvd9zWseWe0z3DnCYJtZUi67WbvcZHyMzhPNZR+EL791xKdKbc6O9T6KiMIQipRhdLnJI+Z49o/XQorh4SnIKoAsXQ3TcFmYgXii0eUm8RKDVn8mNKnpMMD9z+kVqRXiitINAxfYfhdZ1+F4yE3u1sJhuox6Q450X9L0BxCVoHGpZ3meQSUaa4JTHhEWnoVZ0PGkHW7StGIU75W+VLSzF09Ki/eJu6QJtzvmAr3NVuFXSYObVJshWbzXh+ONGQeIJ6shzH0JwJLS1xaaECJJ0u24yeZwG7vSMXqOI2bm7pWi/iOq4waPWXZswltFg5scfQzkkmw2KqZbpAiaIEM5U78rGO1wKQT9jXtu8sEUQkqoH8pNzaCyY4hT0GhrNRW5t+nJotqPR4+bDAWwqDAYXeiiwQpmsi989bo0g6MYB0/liBd/sOcm66Ko3hmvmLvrRYYhsgwI1Am+boTcazPiBELH/qPHTSa3nUYHLzCIBpV9AyWu+QyCGZZxgD/+zVUDZz3UWvHKFzbcZEYgCQ72XMqkC7abGMSDyqQP4yVjg1vyF97XmK5gpVDXc6A+0OZMvfMEbFU0/KGHdjDCQWXiYa0LHCqUvvwmV3ti0UNNLSTpYZCYbznW+xkG2aCyfKXKZ5x1Mkz6gUVDvA0NWtxket9FHtXFALKK4waVmZJhigEgkiuDQdR9wyvg1fWFJ2SgJlxyDNrn1+WDynyuEtvBFW8eYnh5tbGreP5xbTPd4bnXm5XuIB5UFhmcxWBcBWlTMGFOh6sel7/AIJxh+CxfKAaV5Rgs1wzPPA+kGFhn3sJgvPjqnjlEuQP6QWX5zq1YqMXDTmbhyDDAYXR2/zEGx6MIiVkEGPlEU3swZeB79FOyaSul/ehU9pJOVVbjgpvkq96/7KAy8xRVDuxYRybjRXglAWKtNCp7LPUiLBtstxMuSxJcf5NJAhhXl6LCx1jTZGBy511lb5fwE8XTdaeUsJrWGikeqjYcPdCBAV8uuaROZa/RXVq8YTRWwizVPRF35HIFmPEFOiunw6BR2YsKAvbFFAeqPBnN8LiYiOSNhBNoZM4PsxY2lT2EfQXJBhY9H/4TCDIQkCg5EID08RqjVdlLSwb5MQf7smtkwVsnEO1YwTlqW3gh9OejV9kjuS/vsXzJxnEiQiDZz7DrXwX1tF/mheuketes7CX7PXP/G7l3IBQpcI0bFS8Y2At/UG/LjQU32pW9lk2/1SrhzJflmZaoAqQCBmTlwXhJLH+wr+y5ctMRPi5e+TvctfADoTwKYcNp4ItgdvT7sKTq+DuMZmVPF+Ax5FEHwd08MwMRoiCou1MJJfTa8uoVpxUQlEcy2qBX2TNaFA451o9pTbMpm4KpdwSNQIgON6F1bIFBIs6ZnD/YVvasCIH1Ps8X6wAGA4e8M2KTSecuwpxPkvW5bip76aaAN/JaD8ExmRMEGETHy0C3XCA5uW4EWKT9zmVlr8Dg4G/xHlZzCcQcx1FjANV4c0Q9ua4nLl8MQxW1kspehQF+woCz9ZSnh5v6EvepDoKDeWVNDlTbHA0pyJaispdjgF3VrxuyXpeZbNqK/c7GKlVvhvc5V0Joqu/wPjFY35AsfFbZs9mqTKF+1K/fPQUnI7g2xyIUC6dcju9cF7jcEeYYF0bwBbo9V9lzSemKO/hj+jd4robDBAh5ACwgoC2mRV0X51u+O/FB0asWRpNJZc/+AltUTtsfnKv/s9d9ab6EcqCDwB6xBmZvzLYGIGy25NGrwUvAyZ9YAGpafLLxYsOYxN4qBTB0CDjVlCsJ0MWmS+w3VUZAcbIAty0q+DMiRHJS17v5Royh9akdfIR4yhbWWTyV52VBqySCShzyyl7BHBaZ/W6InPwU3iJtNHaRlEOk+MNsV4SX2lOxDNiVkktTiDTGYcUhbGrQn/PlgckyzsYIMwObMSzWkNrVo64RmpNAdxYevGLuzqezQPh2FuwgQMr3CAzo5jesGR7M1PlaA/VqlXfP59sUk8vFwyHMIPrBafFwwaPYN4iwDB+orB7G/A+0i8GS6nLIgu5hfSfXOStusvwNA6yKjIffHR8z3L1SZDwmz1td1he16eUxrK0pyq5zbRcOIhyyTTp8iYEtE7E4ozAvMKKwD25NBXmOmESfUgLRJfUTDKqONvqYkltwXrT9emOoHZfEgL8wIXtpI0RuuK1LxS+x8W7us0Qh+wi4J6HHG/orwqD8LiNOIvGXcT98xzJgaJO0ykKE/c4/5T3l3f2AgTxjGo1NW1JmPBRPaX2sbE34gy7+TTkHUn+XbAzxMdvqTMWjEbkoN0ubCHkmACJ8NC0EeIHBxkJ5eLkAdt57UgkYaKRzc0Si3JexdVk+Vvtsnw9BQH1nlBaa1KkZ7rUrPEQvUZUoqy4/5Pj5XQj5AE2/ph5hUBUnYxAY7/nYZDKBebL4VReu3gNxkR5Nyhr9Rxhg1u0iERhElvBm8gQ9NXSplGsMzrQLUU7CK9DB32HAIc5gYlyuoxgc+TBycto4Mg9KdW6h6w7uinHP9SsMDpXX06MTuYNXu9i1dYVXcKuRkGGnflAecRnyEQaMFi8CVyGL8EUfHKSSvsq8wcwtYBhy8juG90vHWcHVzFmjOvM7DDK6WtqxVDrYtuiQqtnTgDTefQQWf85VPYxCcboZjj/HwMldVK8Mgq1xjWxBHwdRHwx1qio95eHGf2KpKzCeYIAODc10BG2M4fT09zyLdVr1RQykWxsijabHgGdlQrU3SoopkN88xgC7JINFQIlLBvOpU6uewM9SrjhfiKi7wOATad47oXKIUv3PPgZxZrQZbxX1AKtBXhf5BSFRRDS0nOlLYYXBlYSLhHrYEZZeIrHHQOlHd9xteclZU16GPU9T4nm8kpD2YUt3hGsHpxZi6BYxKsq2mzdSJ3KV2reLAdZez7UUcM00E30qkbc9jJaXyfHUX3Ee5Q1HxrLnD4KawwMMqk5DnIzpuGIErPyU5/l7unvLoRC1/VmCwfSFwYUKcF2k0LNPkw6XI8Hgq14dLQyuhx1z/+b5jNeQ5FPyCzPapWxeJWJ2W/WjB2aK7pnCZbd4tkhrX/FBC6Jr+Tv08dq/v0W7yacBM8RijkFMJUHdHkz1bmiX8AYDM67n8I3UacPW97R3pzi47ovjktfKnq0chDO0SDCAqXEF5Fv7oPUs4fFqbzPYX00hl2tRjOI/NeCzNMRxvfnvCUJgXUw/kbxMBZsUVE9OMMXNtxj067UusmD4oVhPdU9j/vBi9+kLI69+bDHQK0BOoggrBU8HwOAZfLeEQI+qv0poGEdQViazN+CmIhQVbzHmUZZ08NQhNMqVm99lOoXi1F2448MxUEiVW8TEio9snrdSS/y0FJ5hABF0Xlui1uPCsE74ns6XxOYtDGYjAZdOQjjGeOQc7q/zvLY72ieP3zEYamyK7ddTYv1r6/h60GSYnF5JnpgVS8HmemuoiOnVfVOFeYfB1ZMKDfw98uyEf0WOAAsM5tF6XBd0SwHm0YVptp7zLzCA9uRTemPYAPizmc4+ITXERB8wNbmH0HOp4GDFHcI6qInnHeu/cmG8wmAGU645A2d8jmPRi5M2UmMkpSBRreIEA8jxZ3BZ/cuCdSIMbQ9KuB/tOilD4TDlJjef9NV7M5Af2aceR7yDzVGK0iOuWiXeYGBHgj3Ucy1edO0SF4lwvdgpQ50T4IRPZPTULDDgUAVsQS6uiBEvrUCPY9gtBNPJj7PrToeH1xnuWKQBdZPBlMMM3d1zPT0LDPTStysDr80gOw5cN6IxCfLJVci9z+s9D97FNWMIlJMAL+UqdhiAuivNjOobcnO42Zh3hwWkg2OSBw8qn7OVAktNt1ovoZ4SU3ASYYDrnFFpmQYDTjZWadgQdlPcYvOvU7o7UWRnoceAeHhCNm82net8PBEVy0gR6//OqgmGKFgsNcqNAe5AU0dI1B0rZg8sw08GpcKX53VWMcXpvAnTbyeYzPOAQk4a7KweiKJ37g9w78JHXHLmZuo1I2Z91xj+eEXZ+Pi7G8K+i2OeZH4dzbYwuCVMiH0i4hY3Mss3R4WBMYRfhFBrNj6voxVEsYkmf/wOeCEWRRU2qzLu2z93QXMox1IF76uLI5tN9A6CQPB8u+lvmz4vtvU6nJ6HSiBFNSG6v3DoiZnNejs7Fh7MKGUZ8wfRsVVxVF1puGY+qh/jPpsdOi5mMACVgrSbOZMr4iOTIvecFRsciqmopW0qrcFDczi2PTY7DM150q9b68OjM8Q2GLDl5fYdtnsmDkb4Bp+j4cm8tHdB4JCqwk3pfXOGwlERUoI/MAaBY56yLor7tHOY/j8w2JFuKIhJnmndkVg61WHz8af/PQY3g0YeqSh5zyxfoSUT3l2fNspQ9i7bNKo96I8xyFLpuwD5G6OIoBpzibbEkPLgt2q3F0xqQtsHEohEL3oP3V+rTUvLdNhgcC9+BlP+dvvRKVRYLDAehC/nAxbnlI6ecE792A4DeKm/aDoOBVo1BPlhEnjiA9h8pzEOzBNpfwFY9bfq4n10ENTZaf68MhjjiI0VWFdGZRYIammxLakESRT8cD8+d08iJoU2T9VouAPzKT1yODzqaWdJorylh68ed8P4KrrHO0j60sG3GMwyJ0bXDHRXdSw9KO4Hcm+AwOCkA74uHunqQ3FTLQxO7p70bgD5YV8tDNjEYOV6+piUryc9iR4CstWVe199Mm8dDBiewLz2xQflEnk/zzCYohtzg4oWv9QU3ASwxgiy76oE+uqPTbRLB9tLYWa9yyvCb/iVNwnHVuAwWUI/DEOBQcKvm2DvpEsfVIz8kRkYzX3BRgBQysA6HEd1zbGbdcpgVKcMkvisarYmdLhJ4DO9KX47ovwI4dkZh3JR+LxoqKNeCvLoDzlrY8bZFAdkditm8EXUN1/eYaFpAFRb8ti4RGmiZr4jvo5YHhbfxSAQFLh4EFmikApL2V8CVKKewY3y5XAYzN4/4dfahcMOBvZqaBApz/JRKtXIQL2JxQdWwagnRzv/a6wFt9OmO8ZbCB5hoA89hNUoIXq0pk80YWIqZR3FkaCvU3KDQRnYz62M1Fzl8hXfwKU6uyy+baaH5jzAgEYmpVU/rRXJ4ge5YnEPjnjvV0GXXQ/v88Z8lW+jJ7oMjD1ZAyQG3MT1R3iSh9uGeTzCQL3F3FRiBUpWuqNUL26sUmGAMpyLMKA++ujUHx5vMMh+yz8ysvfq4kz3IXUi7su1zh6xDv2hoi2ka2stwAiD1GTZFkFbDNDFAKvDeJMv2WYVo06xVOFfYZByYmUCYTHA5j5ChYT3SjQZrcHAcsZtkWEcCr34gkG0TUyuYcvcZc0jjJGyCK2NgZor+DMGi1p7eKSwE6yE3C9ktZyBUOtpnQCvkGuEGwE7i7fA+vN/AkYGnWJjnZK3MFhKrqzTJXwJv1lYNki3/Jsnr/TBL2FXnPbXG/8JBm6wUNn1wj/DAM3ibHipziu59kSgs1y5VCjLII+XIKCJATdSlRgDU4rk5mUkUSK7D/YShN4CQl/hEH2rc1Lckhx2H01K0Xa3iYdmcDyD4EhPo7bbAt/tjNwUFqNywBNTMExnDFqj0MnOtsA/8ojFDdBKNkXLd13+pW65xJP3oB0Dqm3hFQZd8ZkDgd6bM/e3IyqSpDzpVEb0NXHvgy+UxdUKhFvmGNS5lzAgKk2HIm8liOnslykGzwwB+ymdTBcelBdC922FGHAnZ2nYZ+vQxFd+AEhAoDlTfHsxVzRlVNx6ca8MCqUv6jXRs9+1ZcRmdx3PvkZR8CkGR0qmNtyWUJAF2zb/wgo4z9zmCCz6UvVA0tRor4WpZ+gX30cUuuszA0g9xX2qEYj9FYnh9fhz4AiC2PxQGNzkzFFLFGOimQ/eXWDtEoM8pUESHx35qjx/j0f8ficv9m0CuErgoRawOgJ5/MDEaCJtZgtIF7ipYd7yyqE3uChQ4LlN5VJj3vV/egy2Ey5eUSoM+HdzIlk0Wzn5YDNVwBW1uRP1zlksN0+N/vMHGOChG19/Y3+CaV7pGEMtkoLC79MWdLdtVZwB1gERS4L4alPCU0eeyjOD0VXZZNpJdp9e79rppPbvLD6XiSIEBMjdWugVdishudpWAhWwPdlmOFaod3k3Sal/J1DxVL81S9xHphpO3vTGDLK68PYY8XvDpUumXfIxJnN6NQmfAxOUmGcX+SAku9MYpMAgNpPxyhDG2qlGoI4NMYDwsDCZgNf/k9F5vhtKIKpHcWsGHFE5pHeePOSoVibm7DDgqqx8fcR1nk243QFGxx+/G2kcrDLFUS0FxG1qG86sc67IGvwEUYte3oDzyLd4v8OeSrqrxuAcARL3sLGojfojNvBgL2FOEHL5xDGPTpTh9t3RWwmS2eE9OWYd34DQTvNST8LHQYh6lQIDSUHp4ho3NCVqDHDcAxlFGPocA+Ss22MMrtLC2SiNQxTDp07UFlO4GVxSn317z4lQd4S4wlsFEsTT7bHagLEkJsGgK31pcdzhy7znMgMmGAz3W+hm/u3F4CgFLIbwHLio1gc8ebsXI2ODAWCnHlQYtIPmh90zEgFZRhctKEDU1o2Cqj6YHXFnMdDRfI0Bn2ePj9JMN81gT30VRVymR2ohfWLv4T1yaOZO7DoEcV9hmNvsX8s6xvYZcBgvhB4VmzxpzyLsl0K8k/cwSKDGHgMG5AVqDPhA99tYDLLFPa589zB4FoihSHvyYJm7ZDHdjto7Y2gIyu3jrzCg+57ad7KzRfbZYvJs/W5V25XzjZGIOrH8HYPZ7TAAJumEJYU6nq4bYZo7cwHzp5/JiX7/GoPB6puIvpNIVTPxAbtmEE2Fxji86PcxBqBdTkVBE9uSSVqX10NOmrfn3LVNfz+Dy9nBID6xWDEpQc1H2gI3Nfm4IIs0c0IPAx/DeokygVcYpBFEP86LovMgG4A+XCSMTR/kMvStvdcJFtvy6/bagYb+Rb0TqXKHR1paLRxCtBXSW9c4LIfQMAT8BQZRgrJhZxBHZXUvyl3tu0cV+nFLi6J9ECz/iEExdav6EKT4lGZgOF9vCPWRydmnoJbC5A+/nznWcRboxfJ2l7r5ZftG3flQL/2BWk7hHtLUfNmxyYEhoqkITKwHfq5jB4Nmf+UvxwMXHwZsyezCIbq/cIaAA34KdsQMIRrf9/qh1RClR2rXtHUgib64MlweiSEYDJAlwzbHn8Xax0eDR2pxtlGoXTflySSyuKNZF8N9cxyxuMWZgbTTSbunT/FUywu2UHhscSPAgGprIMIETI+WhmFxp1htSCvkUwRAKqkU9rYQz3joyGq1REbECHAcQ6RRkPhTynKMMujZaDQWAXKyxpmuqu0IUoPBgXuABpGcbmoW1JyFBUPsf4VwUAWqFdyS2yq9/WsUri6OCXDNysljrmvUOm2quebD1dIt+Rsecw7Vxg8zKeOafrNWmx9mAmwMITd3GGFHqbC8tDnRqgRLDOwxJJ4E1aqfeVwHzGtkHOXQVR/HoyBCq+NSoe35+Hk/Xq2FMdGPcxQwR5CcE2r8IFCGTCsD6pDvMMA5KT2IH3mdQ9LDgNgKpmDPOLyJEy7VrRNtjHjyyZoq9HICuztDMqDtjmOO2dtjsJtaejgB/5mATYqLPOvWfnmOYgIOyySpiwFTTQcVBqwwYJPjgZHc8HBLhlYvzN30l34QaDHQ9ByMk7+5N0yBWqHFQZeQpJ6YRn8+5ByVAO8OknEmoeVByDfWcAGaMwz1iW06np07ljj4NcUAJm5DQ16hth3Qqhtg3BULj1tmu/JP3BRWzMFRU1iCqbYbe1qglahDJWR2QuGZr7hPKKgolBhQnL1oLBOplDEM95iXl7GJ23zuZCobixYiYt/56DQqE0zJkUT6hhgmXUhVCVpmQKShqxj2kxZTk+oOsw2ED3QCrqog9zz9E0H9IlWnnFJsfR/BHWFqMo3qGkHoKcOm02UxEa8cxjc4eghxTrSoDfkeDS+iDl6Am8v6uQvMWr/cSjHCWSz3fgtX+j6q1ZBvWwgtB/6dh1vxsHZgHxEzyM47sUV6AyghelgJ+paaiEgs4wdjNECgNFrkQqWMSofVudBk8MM3UwXCm1Wfky7RCwLuv2FyDmTEa7QsYaZFGYWCPJumrVQcWwxAGhZQdIYPWnEZI5kEdPkflc61E9AmaX/WWmZrZ9Snhh/nrCrp8y/2w4VbIQbuhAZEZjn0u38VvYaVsRZLiIefkpcggrnnJ8+he849Bh/jpypFvMpiQs+wpc2BX84/NG3mgQwe51F2WmcpabH/CTAA9QYogmSFqccAAAAASUVORK5CYII=') repeat; color: #2D3841; + font-weight: 100; + margin: 0; + } + + table + { + width: 100%%; + color: #BDC8D1; + } + + table th, + table td + { + padding: 8px; + } + + table tr:hover td + { + background-color: #393939; + } + + table th + { + background-color: #191919; + text-align: left !important; + } + + #header + { + clear: both; + padding: 0px 0px 10px 20px; + height: 150px; + } + + #wrapper + { + margin: auto; + width: 800px; + color: #BDC8D1; + } + + #wrapper h1 + { + background-color: #191919; + color: white; + letter-spacing: 2px; + margin: 0px; + padding: 8px 15px; + font-family: 'Lucida Grande',Calibri,Verdana,sans-serif; + font-weight: 100; + } + + .page-content { + padding: 20px; + background-color:#232323; + } + </style> + </head> + <body> + <div id="wrapper"> + <h1>Kivy Styleguide (PEP8) Check</h1> + <div class="page-content"> + diff --git a/external/plyer/tools/pep8checker/pep8kivy.py b/external/plyer/tools/pep8checker/pep8kivy.py new file mode 100644 index 0000000..245928e --- /dev/null +++ b/external/plyer/tools/pep8checker/pep8kivy.py @@ -0,0 +1,109 @@ +import sys +from os import walk +from os.path import isdir, join, abspath, dirname +import pep8 +import time + +htmlmode = False + +pep8_ignores = ( + 'E125', # continuation line does not + # distinguish itself from next logical line + 'E126', # continuation line over-indented for hanging indent + 'E127', # continuation line over-indented for visual indent + 'E128') # continuation line under-indented for visual indent + +class KivyStyleChecker(pep8.Checker): + + def __init__(self, filename): + pep8.Checker.__init__(self, filename, ignore=pep8_ignores) + + def report_error(self, line_number, offset, text, check): + if htmlmode is False: + return pep8.Checker.report_error(self, + line_number, offset, text, check) + + # html generation + print('<tr><td>{0}</td><td>{1}</td></tr>'.format(line_number, text)) + + +if __name__ == '__main__': + + def usage(): + print('Usage: python pep8kivy.py [-html] <file_or_folder_to_check>*') + print('Folders will be checked recursively.') + sys.exit(1) + + if len(sys.argv) < 2: + usage() + if sys.argv[1] == '-html': + if len(sys.argv) < 3: + usage() + else: + htmlmode = True + targets = sys.argv[-1].split() + elif sys.argv == 2: + targets = sys.argv[-1] + else: + targets = sys.argv[-1].split() + + def check(fn): + try: + checker = KivyStyleChecker(fn) + except IOError: + # File couldn't be opened, so was deleted apparently. + # Don't check deleted files. + return 0 + return checker.check_all() + + errors = 0 + exclude_dirs = ['/lib', '/coverage', '/pep8', '/doc'] + exclude_files = ['kivy/gesture.py', 'osx/build.py', 'win32/build.py', + 'kivy/tools/stub-gl-debug.py', + 'kivy/modules/webdebugger.py', + 'kivy/modules/_webdebugger.py'] + for target in targets: + if isdir(target): + if htmlmode: + path = join(dirname(abspath(__file__)), 'pep8base.html') + print(open(path, 'r').read()) + print('''<p>Generated: %s</p><table>''' % (time.strftime('%c'))) + + for dirpath, dirnames, filenames in walk(target): + cont = False + for pat in exclude_dirs: + if pat in dirpath: + cont = True + break + if cont: + continue + for filename in filenames: + if not filename.endswith('.py'): + continue + cont = False + complete_filename = join(dirpath, filename) + for pat in exclude_files: + if complete_filename.endswith(pat): + cont = True + if cont: + continue + + if htmlmode: + print('<tr><th colspan="2">%s</td></tr>' \ + % complete_filename) + errors += check(complete_filename) + + if htmlmode: + print('</div></div></table></body></html>') + + else: + # Got a single file to check + for pat in exclude_dirs + exclude_files: + if pat in target: + break + else: + if target.endswith('.py'): + errors += check(target) + + # If errors is 0 we return with 0. That's just fine. + sys.exit(errors) diff --git a/external/plyer/tools/pep8checker/pre-commit.githook b/external/plyer/tools/pep8checker/pre-commit.githook new file mode 100644 index 0000000..23d119c --- /dev/null +++ b/external/plyer/tools/pep8checker/pre-commit.githook @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +''' + Kivy Git Pre-Commit Hook to Enforce Styleguide + ============================================== + + This script is not supposed to be run directly. + Instead, copy it to your kivy/.git/hooks/ directory, call it 'pre-commit' + and make it executable. + + If you attempt to commit, git will run this script, which in turn will run + the styleguide checker over your code and abort the commit if there are any + errors. If that happens, please fix & retry. + + To install:: + + cp kivy/tools/pep8checker/pre-commit.githook .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit +''' + +import sys, os +from os.path import dirname, abspath, sep, join +from subprocess import call, Popen, PIPE + +curdir = dirname(abspath(__file__)) +kivydir = sep.join(curdir.split(sep)[:-2]) +srcdir = join(kivydir, 'kivy') +script = join(srcdir, 'tools', 'pep8checker', 'pep8kivy.py') +try: + with open(script): pass +except IOError: + # if this not the kivy project, find the script file in the kivy project + os.environ['KIVY_NO_CONSOLELOG'] = '1' + import kivy + script = join(dirname(kivy.__file__), 'tools', 'pep8checker', 'pep8kivy.py') + srcdir = '' + +# Only check the files that were staged +#proc = Popen(['git', 'diff', '--cached', '--name-only', 'HEAD'], stdout=PIPE) +#targets = [join(kivydir, target) for target in proc.stdout] + +# Correction: only check the files that were staged, but do not include +# deleted files. +proc = Popen(['git', 'diff', '--cached', '--name-status', 'HEAD'], stdout=PIPE) +proc.wait() + +# This gives output like the following: +# +# A examples/widgets/lists/list_simple_in_kv.py +# A examples/widgets/lists/list_simple_in_kv_2.py +# D kivy/uix/observerview.py +# +# So check for D entries and remove them from targets. +# +targets = [] +for target in proc.stdout: + parts = [p.strip() for p in target.split()] + if parts[0] != 'D': + targets.append(join(kivydir, target.decode(encoding='UTF-8'))) + +# Untested possibility: After making the changes above for removing deleted +# files from targets, saw also where the git diff call could be: +# +# git diff --cached --name-only --diff-filter=ACM +# (leaving off D) +# +# and we could then remove the special handling in python for targets above. + +call(['git', 'stash', 'save', '--keep-index', '--quiet']) +retval = call([sys.executable, script, srcdir] + targets) +call(['git', 'stash', 'pop', '--quiet']) + +if retval: + # There are styleguide violations + print("Error:", retval, "styleguide violation(s) encountered!") + print("Your commit has been aborted. Please fix the violations and retry.") + sys.exit(retval) + diff --git a/external/plyer/utils.py b/external/plyer/utils.py new file mode 100644 index 0000000..347c8d5 --- /dev/null +++ b/external/plyer/utils.py @@ -0,0 +1,135 @@ +''' +Utils +===== + +''' + +__all__ = ('platform', ) + +from os import environ +from os import path +from sys import platform as _sys_platform + + +class Platform(object): + # refactored to class to allow module function to be replaced + # with module variable + + def __init__(self): + self._platform_ios = None + self._platform_android = None + + def __eq__(self, other): + return other == self._get_platform() + + def __ne__(self, other): + return other != self._get_platform() + + def __str__(self): + return self._get_platform() + + def __repr__(self): + return 'platform name: \'{platform}\' from: \n{instance}'.format( + platform=self._get_platform(), + instance=super(Platform, self).__repr__() + ) + + def __hash__(self): + return self._get_platform().__hash__() + + def _get_platform(self): + + if self._platform_android is None: + # ANDROID_ARGUMENT and ANDROID_PRIVATE are 2 environment variables + # from python-for-android project + self._platform_android = 'ANDROID_ARGUMENT' in environ + + if self._platform_ios is None: + self._platform_ios = (environ.get('KIVY_BUILD', '') == 'ios') + + # On android, _sys_platform return 'linux2', so prefer to check the + # import of Android module than trying to rely on _sys_platform. + if self._platform_android is True: + return 'android' + elif self._platform_ios is True: + return 'ios' + elif _sys_platform in ('win32', 'cygwin'): + return 'win' + elif _sys_platform == 'darwin': + return 'macosx' + elif _sys_platform[:5] == 'linux': + return 'linux' + return 'unknown' + + +platform = Platform() + + +class Proxy(object): + # taken from http://code.activestate.com/recipes/496741-object-proxying/ + + __slots__ = ['_obj', '_name', '_facade'] + + def __init__(self, name, facade): + object.__init__(self) + object.__setattr__(self, '_obj', None) + object.__setattr__(self, '_name', name) + object.__setattr__(self, '_facade', facade) + + def _ensure_obj(self): + obj = object.__getattribute__(self, '_obj') + if obj: + return obj + # do the import + try: + name = object.__getattribute__(self, '_name') + module = 'plyer.platforms.{}.{}'.format( + platform, name) + mod = __import__(module, fromlist='.') + obj = mod.instance() + except: + import traceback + traceback.print_exc() + facade = object.__getattribute__(self, '_facade') + obj = facade() + + object.__setattr__(self, '_obj', obj) + return obj + + def __getattribute__(self, name): + if name == '__doc__': + return + object.__getattribute__(self, '_ensure_obj')() + return getattr(object.__getattribute__(self, '_obj'), name) + + def __delattr__(self, name): + object.__getattribute__(self, '_ensure_obj')() + delattr(object.__getattribute__(self, '_obj'), name) + + def __setattr__(self, name, value): + object.__getattribute__(self, '_ensure_obj')() + setattr(object.__getattribute__(self, '_obj'), name, value) + + def __bool__(self): + object.__getattribute__(self, '_ensure_obj')() + return bool(object.__getattribute__(self, '_obj')) + + def __str__(self): + object.__getattribute__(self, '_ensure_obj')() + return str(object.__getattribute__(self, '_obj')) + + def __repr__(self): + object.__getattribute__(self, '_ensure_obj')() + return repr(object.__getattribute__(self, '_obj')) + + +def whereis_exe(program): + ''' Tries to find the program on the system path. + Returns the path if it is found or None if it's not found. + ''' + path_split = ';' if platform == 'win' else ':' + for p in environ.get('PATH', '').split(path_split): + if path.exists(path.join(p, program)) and \ + not path.isdir(path.join(p, program)): + return path.join(p, program) + return None diff --git a/external/plyer/utils.pyc b/external/plyer/utils.pyc Binary files differnew file mode 100644 index 0000000..ff2d89a --- /dev/null +++ b/external/plyer/utils.pyc diff --git a/external/pytmx/LICENSE b/external/pytmx/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/external/pytmx/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/external/pytmx/__init__.py b/external/pytmx/__init__.py new file mode 100644 index 0000000..46c8707 --- /dev/null +++ b/external/pytmx/__init__.py @@ -0,0 +1,19 @@ +import logging + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +from .pytmx import * +try: + from pytmx.util_pygame import load_pygame +except ImportError: + logger.debug('cannot import pygame tools') + + +__version__ = (3, 20, 15) +__author__ = 'bitcraft' +__author_email__ = 'leif.theden@gmail.com' +__description__ = 'Map loader for TMX Files - Python 2 and 3' diff --git a/external/pytmx/pytmx.py b/external/pytmx/pytmx.py new file mode 100644 index 0000000..61120a0 --- /dev/null +++ b/external/pytmx/pytmx.py @@ -0,0 +1,1115 @@ +import logging +import six +import os +from itertools import chain, product +from collections import defaultdict, namedtuple +from xml.etree import ElementTree +from six.moves import zip, map +from operator import attrgetter + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +__all__ = ['TiledElement', + 'TiledMap', + 'TiledTileset', + 'TiledTileLayer', + 'TiledObject', + 'TiledObjectGroup', + 'TiledImageLayer', + 'TileFlags', + 'convert_to_bool', + 'parse_properties'] + +# internal flags +TRANS_FLIPX = 1 +TRANS_FLIPY = 2 +TRANS_ROT = 4 + +# Tiled gid flags +GID_TRANS_FLIPX = 1 << 31 +GID_TRANS_FLIPY = 1 << 30 +GID_TRANS_ROT = 1 << 29 + +# error message format strings go here +duplicate_name_fmt = 'Cannot set user {} property on {} "{}"; Tiled property already exists.' + +flag_names = ( + 'flipped_horizontally', + 'flipped_vertically', + 'flipped_diagonally',) + +TileFlags = namedtuple('TileFlags', flag_names) +AnimationFrame = namedtuple('AnimationFrame', ['gid', 'duration']) + + +def default_image_loader(filename, flags, **kwargs): + """ This default image loader just returns filename, rect, and any flags + """ + def load(rect=None, flags=None): + return filename, rect, flags + + return load + + +def decode_gid(raw_gid): + """ Decode a GID from TMX data + + as of 0.7.0 it determines if the tile should be flipped when rendered + as of 0.8.0 bit 30 determines if GID is rotated + + :param raw_gid: 32-bit number from TMX layer data + :return: gid, flags + """ + flags = TileFlags( + raw_gid & GID_TRANS_FLIPX == GID_TRANS_FLIPX, + raw_gid & GID_TRANS_FLIPY == GID_TRANS_FLIPY, + raw_gid & GID_TRANS_ROT == GID_TRANS_ROT) + gid = raw_gid & ~(GID_TRANS_FLIPX | GID_TRANS_FLIPY | GID_TRANS_ROT) + return gid, flags + + +def convert_to_bool(text): + """ Convert a few common variations of "true" and "false" to boolean + + :param text: string to test + :return: boolean + :raises: ValueError + """ + try: + return bool(int(text)) + except: + pass + + text = str(text).lower() + if text == "true": + return True + if text == "yes": + return True + if text == "false": + return False + if text == "no": + return False + + raise ValueError + +# used to change the unicode string returned from xml to +# proper python variable types. +types = defaultdict(lambda: str) +types.update({ + "version": float, + "orientation": str, + "width": float, + "height": float, + "tilewidth": int, + "tileheight": int, + "firstgid": int, + "source": str, + "name": str, + "spacing": int, + "margin": int, + "trans": str, + "id": int, + "opacity": float, + "visible": convert_to_bool, + "encoding": str, + "compression": str, + "gid": int, + "type": str, + "x": float, + "y": float, + "value": str, + "rotation": float, +}) + + +def parse_properties(node): + """ Parse a Tiled xml node and return a dict that represents a tiled "property" + + :param node: etree element + :return: dict + """ + d = dict() + for child in node.findall('properties'): + for subnode in child.findall('property'): + d[subnode.get('name')] = subnode.get('value') + return d + + +class TiledElement(object): + """ Base class for all pytmx types + """ + allow_duplicate_names = False + + def __init__(self): + self.properties = dict() + + @classmethod + def from_xml_string(cls, xml_string): + """Return a TileElement object from a xml string + + :param xml_string: string containing xml data + :rtype: TiledElement instance + """ + return cls().parse_xml(ElementTree.fromstring(xml_string)) + + def _cast_and_set_attributes_from_node_items(self, items): + for key, value in items: + casted_value = types[key](value) + setattr(self, key, casted_value) + + def _contains_invalid_property_name(self, items): + if self.allow_duplicate_names: + return False + + for k, v in items: + if hasattr(self, k): + msg = duplicate_name_fmt.format(k, self.__class__.__name__, self.name) + logger.error(msg) + return True + return False + + @staticmethod + def _log_property_error_message(self): + msg = 'Some name are reserved for {0} objects and cannot be used.' + logger.error(msg) + + def _set_properties(self, node): + """ Create dict containing Tiled object attributes from xml data + + read the xml attributes and tiled "properties" from a xml node and fill + in the values into the object's dictionary. Names will be checked to + make sure that they do not conflict with reserved names. + + :param node: etree element + :return: dict + """ + self._cast_and_set_attributes_from_node_items(node.items()) + properties = parse_properties(node) + if (not self.allow_duplicate_names and + self._contains_invalid_property_name(properties.items())): + self._log_property_error_message() + raise ValueError + + self.properties = properties + + def __getattr__(self, item): + try: + return self.properties[item] + except KeyError: + raise AttributeError + + def __repr__(self): + return '<{0}: "{1}">'.format(self.__class__.__name__, self.name) + + +class TiledMap(TiledElement): + """Contains the layers, objects, and images from a Tiled TMX map + + This class is meant to handle most of the work you need to do to use a map. + """ + + def __init__(self, filename=None, image_loader=default_image_loader, **kwargs): + """ Create new TiledMap + + :param filename: filename of tiled map to load + :param image_loader: function that will load images (see below) + :param optional_gids: load specific tile image GID, even if never used + :param invert_y: invert the y axis + :param load_all_tiles: load all tile images, even if never used + :param allow_duplicate_names: allow duplicates in objects' metatdata + + image_loader: + this must be a reference to a function that will accept a tuple: + (filename of image, bounding rect of tile in image, flags) + the function must return a reference to to the tile. + """ + TiledElement.__init__(self) + self.filename = filename + self.image_loader = image_loader + + # optional keyword arguments checked here + self.optional_gids = kwargs.get('optional_gids', set()) + self.load_all_tiles = kwargs.get('load_all', False) + self.invert_y = kwargs.get('invert_y', True) + + # allow duplicate names to be parsed and loaded + TiledElement.allow_duplicate_names = \ + kwargs.get('allow_duplicate_names', False) + + self.layers = list() # all layers in proper order + self.tilesets = list() # TiledTileset objects + self.tile_properties = dict() # tiles that have properties + self.layernames = dict() + + # only used tiles are actually loaded, so there will be a difference + # between the GIDs in the Tiled map data (tmx) and the data in this + # object and the layers. This dictionary keeps track of that. + self.gidmap = defaultdict(list) + self.imagemap = dict() # mapping of gid and trans flags to real gids + self.tiledgidmap = dict() # mapping of tiledgid to pytmx gid + self.maxgid = 1 + + # should be filled in by a loader function + self.images = list() + + # defaults from the TMX specification + self.version = 0.0 + self.orientation = None + self.width = 0 # width of map in tiles + self.height = 0 # height of map in tiles + self.tilewidth = 0 # width of a tile in pixels + self.tileheight = 0 # height of a tile in pixels + self.background_color = None + + # initialize the gid mapping + self.imagemap[(0, 0)] = 0 + + if filename: + self.parse_xml(ElementTree.parse(self.filename).getroot()) + + def __repr__(self): + return '<{0}: "{1}">'.format(self.__class__.__name__, self.filename) + + # iterate over layers and objects in map + def __iter__(self): + return chain(self.layers, self.objects) + + def _set_properties(self, node): + TiledElement._set_properties(self, node) + + # TODO: make class/layer-specific type casting + # layer height and width must be int, but TiledElement.set_properties() + # make a float by default, so recast as int here + self.height = int(self.height) + self.width = int(self.width) + + def parse_xml(self, node): + """ Parse a map from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + self._set_properties(node) + self.background_color = node.get('backgroundcolor', + self.background_color) + + # *** do not change this load order! *** # + # *** gid mapping errors will occur if changed *** # + for subnode in node.findall('layer'): + self.add_layer(TiledTileLayer(self, subnode)) + + for subnode in node.findall('imagelayer'): + self.add_layer(TiledImageLayer(self, subnode)) + + for subnode in node.findall('objectgroup'): + self.add_layer(TiledObjectGroup(self, subnode)) + + for subnode in node.findall('tileset'): + self.add_tileset(TiledTileset(self, subnode)) + + # "tile objects", objects with a GID, have need to have their attributes + # set after the tileset is loaded, so this step must be performed last + # also, this step is performed for objects to load their tiles. + # height and width will automatically be set according to the tileset + # that the image is from + + # tiled stores the origin of GID objects by the lower right corner + # this is different for all other types, so i just adjust it here + # so all types loaded with pytmx are uniform. + + # iterate through tile objects and handle the image + for o in [o for o in self.objects if o.gid]: + + # gids might also have properties assigned to them + # in that case, assign the gid properties to the object as well + p = self.get_tile_properties_by_gid(o.gid) + if p: + o.properties.update(p) + + try: + tileset = self.get_tileset_from_gid(o.gid) + except ValueError: + msg = 'attempted to lookup invalid gid %s in object %s' + logger.error(msg, o.gid, o) + else: + if self.invert_y: + o.y -= tileset.tileheight + o.height = tileset.tileheight + o.width = tileset.tilewidth + + self.reload_images() + return self + + def reload_images(self): + """ Load the map images from disk + + This method will use the image loader passed in the constructor + to do the loading or will use a generic default, in which case no + images will be loaded. + + :return: None + """ + self.images = [None] * self.maxgid + + # iterate through tilesets to get source images + for ts in self.tilesets: + + # skip tilesets without a source + if ts.source is None: + continue + + path = os.path.join(os.path.dirname(self.filename), ts.source) + colorkey = getattr(ts, 'trans', None) + loader = self.image_loader(path, colorkey) + + p = product(range(ts.margin, + ts.height + ts.margin - ts.tileheight + 1, + ts.tileheight + ts.spacing), + range(ts.margin, + ts.width + ts.margin - ts.tilewidth + 1, + ts.tilewidth + ts.spacing)) + + # iterate through the tiles + for real_gid, (y, x) in enumerate(p, ts.firstgid): + rect = (x, y, ts.tilewidth, ts.tileheight) + gids = self.map_gid(real_gid) + + # gids is None if the tile is never used + # but give another chance to load the gid anyway + if gids is None: + if self.load_all_tiles or real_gid in self.optional_gids: + # TODO: handle flags? - might never be an issue, though + gids = [self.register_gid(real_gid, flags=0)] + + if gids: + # flags might rotate/flip the image, so let the loader + # handle that here + for gid, flags in gids: + self.images[gid] = loader(rect, flags) + + # load image layer images + for layer in (i for i in self.layers if isinstance(i, TiledImageLayer)): + source = getattr(layer, 'source', None) + if source: + colorkey = getattr(layer, 'trans', None) + real_gid = len(self.images) + gid = self.register_gid(real_gid) + layer.gid = gid + path = os.path.join(os.path.dirname(self.filename), source) + loader = self.image_loader(path, colorkey) + image = loader() + self.images.append(image) + + # load images in tiles. + # instead of making a new gid, replace the reference to the tile that + # was loaded from the tileset + for real_gid, props in self.tile_properties.items(): + source = props.get('source', None) + if source: + colorkey = props.get('trans', None) + path = os.path.join(os.path.dirname(self.filename), source) + loader = self.image_loader(path, colorkey) + image = loader() + self.images[real_gid] = image + + def get_tile_image(self, x, y, layer): + """ Return the tile image for this location + + :param x: x coordinate + :param y: y coordinate + :param layer: layer number + :rtype: surface if found, otherwise 0 + """ + try: + assert (x >= 0 and y >= 0) + except AssertionError: + raise ValueError + + try: + layer = self.layers[layer] + except IndexError: + raise ValueError + + assert (isinstance(layer, TiledTileLayer)) + + try: + gid = layer.data[y][x] + except (IndexError, ValueError): + raise ValueError + except TypeError: + msg = "Tiles must be specified in integers." + logger.debug(msg) + raise TypeError + + else: + return self.get_tile_image_by_gid(gid) + + def get_tile_image_by_gid(self, gid): + """ Return the tile image for this location + + :param gid: GID of image + :rtype: surface if found, otherwise ValueError + """ + try: + assert (int(gid) >= 0) + return self.images[gid] + except TypeError: + msg = "GIDs must be expressed as a number. Got: {0}" + logger.debug(msg.format(gid)) + raise TypeError + except (AssertionError, IndexError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + logger.debug(msg.format(gid)) + raise ValueError + + def get_tile_gid(self, x, y, layer): + """ Return the tile image GID for this location + + :param x: x coordinate + :param y: y coordinate + :param layer: layer number + :rtype: surface if found, otherwise ValueError + """ + try: + assert (x >= 0 and y >= 0 and layer >= 0) + except AssertionError: + raise ValueError + + try: + return self.layers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid" + logger.debug(msg, (x, y, layer)) + raise ValueError + + def get_tile_properties(self, x, y, layer): + """ Return the tile image GID for this location + + :param x: x coordinate + :param y: y coordinate + :param layer: layer number + :rtype: python dict if found, otherwise None + """ + try: + assert (x >= 0 and y >= 0 and layer >= 0) + except AssertionError: + raise ValueError + + try: + gid = self.layers[int(layer)].data[int(y)][int(x)] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} is invalid." + logger.debug(msg.format(x, y, layer)) + raise Exception + + else: + try: + return self.tile_properties[gid] + except (IndexError, ValueError): + msg = "Coords: ({0},{1}) in layer {2} has invalid GID: {3}" + logger.debug(msg.format(x, y, layer, gid)) + raise Exception + except KeyError: + return None + + def get_tile_locations_by_gid(self, gid): + """ Search map for tile locations by the GID + + Note: Not a fast operation. Cache results if used often. + + :param gid: GID to be searched for + :rtype: generator of tile locations + """ + # use this func to make sure GID is valid + self.get_tile_image_by_gid(gid) + + p = product(range(self.width), + range(self.height), + range(len(self.layers))) + + return ((x, y, l) for (x, y, l) in p if + self.layers[l].data[y][x] == gid) + + def get_tile_properties_by_gid(self, gid): + """ Get the tile properties of a tile GID + + :param gid: GID + :rtype: python dict if found, otherwise None + """ + try: + return self.tile_properties[gid] + except KeyError: + return None + + def set_tile_properties(self, gid, properties): + """ Set the tile properties of a tile GID + + :param gid: GID + :param properties: python dict of properties for GID + """ + self.tile_properties[gid] = properties + + def get_tile_properties_by_layer(self, layer): + """ Get the tile properties of each GID in layer + + :param layer: layer number + :rtype: iterator of (gid, properties) tuples + """ + try: + assert (int(layer) >= 0) + layer = int(layer) + except (TypeError, AssertionError): + msg = "Layer must be a positive integer. Got {0} instead." + logger.debug(msg.format(type(layer))) + raise ValueError + + p = product(range(self.width), range(self.height)) + layergids = set(self.layers[layer].data[y][x] for x, y in p) + + for gid in layergids: + try: + yield gid, self.tile_properties[gid] + except KeyError: + continue + + def add_layer(self, layer): + """ Add a layer (TileTileLayer, TiledImageLayer, or TiledObjectGroup) + + :param layer: TileTileLayer, TiledImageLayer, TiledObjectGroup object + """ + assert ( + isinstance(layer, + (TiledTileLayer, TiledImageLayer, TiledObjectGroup))) + + self.layers.append(layer) + self.layernames[layer.name] = layer + + def add_tileset(self, tileset): + """ Add a tileset to the map + + :param tileset: TiledTileset + """ + assert (isinstance(tileset, TiledTileset)) + self.tilesets.append(tileset) + + def get_layer_by_name(self, name): + """Return a layer by name + + :param name: Name of layer. Case-sensitive. + :rtype: Layer object if found, otherwise ValueError + """ + try: + return self.layernames[name] + except KeyError: + msg = 'Layer "{0}" not found.' + logger.debug(msg.format(name)) + raise ValueError + + def get_object_by_name(self, name): + """Find an object + + :param name: Name of object. Case-sensitive. + :rtype: Object if found, otherwise ValueError + """ + for obj in self.objects: + if obj.name == name: + return obj + raise ValueError + + def get_tileset_from_gid(self, gid): + """ Return tileset that owns the gid + + Note: this is a slow operation, so if you are expecting to do this + often, it would be worthwhile to cache the results of this. + + :param gid: gid of tile image + :rtype: TiledTileset if found, otherwise ValueError + """ + try: + tiled_gid = self.tiledgidmap[gid] + except KeyError: + raise ValueError + + for tileset in sorted(self.tilesets, key=attrgetter('firstgid'), + reverse=True): + if tiled_gid >= tileset.firstgid: + return tileset + + raise ValueError + + @property + def objectgroups(self): + """Return iterator of all object groups + + :rtype: Iterator + """ + return (layer for layer in self.layers + if isinstance(layer, TiledObjectGroup)) + + @property + def objects(self): + """Return iterator of all the objects associated with this map + + :rtype: Iterator + """ + return chain(*self.objectgroups) + + @property + def visible_layers(self): + """Return iterator of Layer objects that are set 'visible' + + :rtype: Iterator + """ + return (l for l in self.layers if l.visible) + + @property + def visible_tile_layers(self): + """Return iterator of layer indexes that are set 'visible' + + :rtype: Iterator + """ + return (i for (i, l) in enumerate(self.layers) + if l.visible and isinstance(l, TiledTileLayer)) + + @property + def visible_object_groups(self): + """Return iterator of object group indexes that are set 'visible' + + :rtype: Iterator + """ + return (i for (i, l) in enumerate(self.layers) + if l.visible and isinstance(l, TiledObjectGroup)) + + def register_gid(self, tiled_gid, flags=None): + """ Used to manage the mapping of GIDs between the tmx and pytmx + + :param tiled_gid: GID that is found in TMX data + :rtype: GID that pytmx uses for the the GID passed + """ + if flags is None: + flags = TileFlags(0, 0, 0) + + if tiled_gid: + try: + return self.imagemap[(tiled_gid, flags)][0] + except KeyError: + gid = self.maxgid + self.maxgid += 1 + self.imagemap[(tiled_gid, flags)] = (gid, flags) + self.gidmap[tiled_gid].append((gid, flags)) + self.tiledgidmap[gid] = tiled_gid + return gid + + else: + return 0 + + def map_gid(self, tiled_gid): + """ Used to lookup a GID read from a TMX file's data + + :param tiled_gid: GID that is found in TMX data + :rtype: (GID, flags) for the the GID passed, None if not found + """ + try: + return self.gidmap[int(tiled_gid)] + except KeyError: + return None + except TypeError: + msg = "GIDs must be an integer" + logger.debug(msg) + raise TypeError + + +class TiledTileset(TiledElement): + """ Represents a Tiled Tileset + + External tilesets are supported. GID/ID's from Tiled are not guaranteed to + be the same after loaded. + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.offset = (0, 0) + + # defaults from the specification + self.firstgid = 0 + self.source = None + self.name = None + self.tilewidth = 0 + self.tileheight = 0 + self.spacing = 0 + self.margin = 0 + self.trans = None + self.width = 0 + self.height = 0 + + self.parse_xml(node) + + def parse_xml(self, node): + """ Parse a Tileset from ElementTree xml element + + A bit of mangling is done here so that tilesets that have external + TSX files appear the same as those that don't + + :param node: ElementTree element + :return: self + """ + import os + + # if true, then node references an external tileset + source = node.get('source', None) + if source: + if source[-4:].lower() == ".tsx": + + # external tilesets don't save this, store it for later + self.firstgid = int(node.get('firstgid')) + + # we need to mangle the path - tiled stores relative paths + dirname = os.path.dirname(self.parent.filename) + path = os.path.abspath(os.path.join(dirname, source)) + try: + node = ElementTree.parse(path).getroot() + except IOError: + msg = "Cannot load external tileset: {0}" + logger.error(msg.format(path)) + raise Exception + + else: + msg = "Found external tileset, but cannot handle type: {0}" + logger.error(msg.format(self.source)) + raise Exception + + self._set_properties(node) + + # since tile objects [probably] don't have a lot of metadata, + # we store it separately in the parent (a TiledMap instance) + register_gid = self.parent.register_gid + for child in node.getiterator('tile'): + tiled_gid = int(child.get("id")) + p = parse_properties(child) + + # handle tiles that have their own image + image = child.find('image') + if image is None: + p['width'] = self.tilewidth + p['height'] = self.tileheight + else: + p['source'] = image.get('source') + p['trans'] = image.get('trans', None) + p['width'] = image.get('width') + p['height'] = image.get('height') + + # handle tiles with animations + anim = child.find('animation') + frames = list() + p['frames'] = frames + if anim is not None: + for frame in anim.findall("frame"): + duration = int(frame.get('duration')) + gid = register_gid(int(frame.get('tileid')) + self.firstgid) + frames.append(AnimationFrame(gid, duration)) + + for gid, flags in self.parent.map_gid(tiled_gid + self.firstgid): + self.parent.set_tile_properties(gid, p) + + # handle the optional 'tileoffset' node + self.offset = node.find('tileoffset') + if self.offset is None: + self.offset = (0, 0) + else: + self.offset = (self.offset.get('x', 0), self.offset.get('y', 0)) + + image_node = node.find('image') + if image_node is not None: + self.source = image_node.get('source') + self.trans = image_node.get('trans', None) + self.width = int(image_node.get('width')) + self.height = int(image_node.get('height')) + + return self + + +class TiledTileLayer(TiledElement): + """ Represents a TileLayer + + To just get the tile images, use TiledTileLayer.tiles() + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.data = list() + + # defaults from the specification + self.name = None + self.opacity = 1.0 + self.visible = True + self.height = 0 + self.width = 0 + + self.parse_xml(node) + + def __iter__(self): + return self.iter_data() + + def iter_data(self): + """ Iterate over layer data + + Yields X, Y, GID tuples for each tile in the layer + + :return: Generator + """ + for y, x in product(range(self.height), range(self.width)): + yield x, y, self.data[y][x] + + def tiles(self): + """ Iterate over tile images of this layer + + This is an optimised generator function that returns + (tile_x, tile_y, tile_image) tuples, + + :rtype: Generator + :return: (x, y, image) tuples + """ + images = self.parent.images + data = self.data + for y, row in enumerate(data): + for x, gid in enumerate(row): + if gid: + yield x, y, images[gid] + + def _set_properties(self, node): + TiledElement._set_properties(self, node) + + # TODO: make class/layer-specific type casting + # layer height and width must be int, but TiledElement.set_properties() + # make a float by default, so recast as int here + self.height = int(self.height) + self.width = int(self.width) + + def parse_xml(self, node): + """ Parse a Tile Layer from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + import struct + import array + + self._set_properties(node) + data = None + next_gid = None + data_node = node.find('data') + + encoding = data_node.get('encoding', None) + if encoding == 'base64': + from base64 import b64decode + + data = b64decode(data_node.text.strip()) + + elif encoding == 'csv': + next_gid = map(int, "".join( + line.strip() for line in data_node.text.strip() + ).split(",")) + + elif encoding: + msg = 'TMX encoding type: {0} is not supported.' + logger.error(msg.format(encoding)) + raise Exception + + compression = data_node.get('compression', None) + if compression == 'gzip': + import gzip + + with gzip.GzipFile(fileobj=six.BytesIO(data)) as fh: + data = fh.read() + + elif compression == 'zlib': + import zlib + + data = zlib.decompress(data) + + elif compression: + msg = 'TMX compression type: {0} is not supported.' + logger.error(msg.format(compression)) + raise Exception + + # if data is None, then it was not decoded or decompressed, so + # we assume here that it is going to be a bunch of tile elements + # TODO: this will/should raise an exception if there are no tiles + if encoding == next_gid is None: + def get_children(parent): + for child in parent.findall('tile'): + yield int(child.get('gid')) + + next_gid = get_children(data_node) + + elif data: + if type(data) == bytes: + fmt = struct.Struct('<L') + iterator = (data[i:i + 4] for i in range(0, len(data), 4)) + next_gid = (fmt.unpack(i)[0] for i in iterator) + else: + msg = 'layer data not in expected format ({})' + logger.error(msg.format(type(data))) + raise Exception + + init = lambda: [0] * self.width + reg = self.parent.register_gid + + # H (16-bit) may be a limitation for very detailed maps + self.data = tuple(array.array('H', init()) for i in range(self.height)) + for (y, x) in product(range(self.height), range(self.width)): + self.data[y][x] = reg(*decode_gid(next(next_gid))) + + return self + + +class TiledObject(TiledElement): + """ Represents a any Tiled Object + + Supported types: Box, Ellipse, Tile Object, Polyline, Polygon + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.type = None + self.x = 0 + self.y = 0 + self.width = 0 + self.height = 0 + self.rotation = 0 + self.gid = 0 + self.visible = 1 + + self.parse_xml(node) + + @property + def image(self): + if self.gid: + return self.parent.images[self.gid] + return None + + def parse_xml(self, node): + """ Parse an Object from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + + def read_points(text): + """parse a text string of float tuples and return [(x,...),...] + """ + return tuple(tuple(map(float, i.split(','))) for i in text.split()) + + self._set_properties(node) + + # correctly handle "tile objects" (object with gid set) + if self.gid: + self.gid = self.parent.register_gid(self.gid) + + points = None + polygon = node.find('polygon') + if polygon is not None: + points = read_points(polygon.get('points')) + self.closed = True + + polyline = node.find('polyline') + if polyline is not None: + points = read_points(polyline.get('points')) + self.closed = False + + if points: + x1 = x2 = y1 = y2 = 0 + for x, y in points: + if x < x1: x1 = x + if x > x2: x2 = x + if y < y1: y1 = y + if y > y2: y2 = y + self.width = abs(x1) + abs(x2) + self.height = abs(y1) + abs(y2) + self.points = tuple( + [(i[0] + self.x, i[1] + self.y) for i in points]) + + return self + + +class TiledObjectGroup(TiledElement, list): + """ Represents a Tiled ObjectGroup + + Supports any operation of a normal list. + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + + # defaults from the specification + self.name = None + self.color = None + self.opacity = 1 + self.visible = 1 + + self.parse_xml(node) + + def parse_xml(self, node): + """ Parse an Object Group from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + self._set_properties(node) + self.extend(TiledObject(self.parent, child) + for child in node.findall('object')) + + return self + + +class TiledImageLayer(TiledElement): + """ Represents Tiled Image Layer + + The image associated with this layer will be loaded and assigned a GID. + """ + + def __init__(self, parent, node): + TiledElement.__init__(self) + self.parent = parent + self.source = None + self.trans = None + self.gid = 0 + + # defaults from the specification + self.name = None + self.opacity = 1 + self.visible = 1 + + self.parse_xml(node) + + @property + def image(self): + if self.gid: + return self.parent.images[self.gid] + return None + + def parse_xml(self, node): + """ Parse an Image Layer from ElementTree xml node + + :param node: ElementTree xml node + :return: self + """ + self._set_properties(node) + self.name = node.get('name', None) + self.opacity = node.get('opacity', self.opacity) + self.visible = node.get('visible', self.visible) + image_node = node.find('image') + self.source = image_node.get('source') + self.trans = image_node.get('trans', None) + return self diff --git a/external/pytmx/readme.md b/external/pytmx/readme.md new file mode 100644 index 0000000..459d9c0 --- /dev/null +++ b/external/pytmx/readme.md @@ -0,0 +1,586 @@ +## PyTMX +##### For Python 2.7 and 3.3+ + +This is the most up-to-date version of PyTMX available and works with +Python 2.7 and 3.3+. + +If you have any problems or suggestions, please open an issue. +I am also often lurking #pygame on freenode. Feel free to contact me. + +Requires the six module. + +*Released under the LGPL v3* + +### See the "apps" folder for example use and cut/paste code. + + +News +=============================================================================== + +__11/13/15__ - Animations are now loaded +__07/08/15__ - Documentation overhaul +__04/18/15__ - Document support for pysdl2 and pyglet +__09/14/14__ - Merge python3 branch. Now 100% compatible with 2.7 and 3.3+ +__07/26/14__ - New python3/2 release. Check it out in the python3 branch. +__05/29/14__ - Added support for rotated objects and floating point +__04/04/14__ - New Six Branch created +__02/28/14__ - Image layer support, object points changed, new test.py! +__02/24/14__ - New Python 3 Support: see python3 branch +__02/06/14__ - Python 3 support coming soon + + +Introduction +=============================================================================== + +PyTMX is a map loader for python/pygame designed for games. It provides smart +tile loading with a fast and efficient storage base. Not only does it +correctly handle most Tiled object types, it also will load metadata for +them so you can modify your maps and objects in Tiled instead of modifying +your source code. + +New support for pysdl2 and pyglet! Check it out! + +Because PyTMX was built with games in mind, it differs slightly from Tiled in +a few minor aspects: + +- Layers not aligned to the grid are not supported. +- Some object metadata attribute names are not supported (see "Reserved Names") + +PyTMX strives to balance performance and flexibility. Feel free to use the +classes provided in pytmx.py as superclasses for your own maps, or simply +load the data with PyTMX and copy the data into your own classes with the api. + +There is no save feature. Once the map is loaded, it will be up to +you to provide a way to save changes to the map. I've used the pickle module +with good results. + +I need to clarify a few things: +- pytmx is not a rendering engine +- pytmx is not the Tiled Map Editor + + +Documentation +=============================================================================== + +This readme does include some detailed documentation, but the full API reference +and documentation can be found at the site below. For examples of real use, +check out the apps folder in this repo. The 'test' apps demonstrate how to +load maps, get layer, tile, and object data, as well as some rendering. + +http://pytmx.readthedocs.org/ + + +# Table of Contents +1. [Installation](#installation) +2. [Basic Use](#basic-use) +3. [Getting Properties](#object-properties) +4. [Working with Maps](#working-with-maps) +5. [Loading from XML](#loading-from-xml) +6. [Custom Image Loading](#custom-image-loading) +7. [Working with Tile Layers](#working-with-tile-layers) +8. [Getting Tile Animations](#getting-tile-animations) +9. [Working with Objects](#working-with-objects) +10. [Understanding Properties](#understanding-properties) + + +Getting Help +=============================================================================== + +For bugs or feature requests, please use the issues feature of github. For +all other general questions, join me on IRC at freennode.net #pygame. + + +Design Goals and Features +=============================================================================== + +* API with many handy functions +* Memory efficient and performant +* Loads data, "properties" metadata, and images from Tiled's TMX format +* Supports base64, csv, gzip, zlib and uncompressed XML formats +* Properties for all native Tiled object types +* Point data for polygon and polyline objects +* Automatic flipping and rotation of tiles +* Built-in image loading with pygame, pysdl2, and pyglet +* Loads animation information + + +Why use PyTMX? +=============================================================================== + +### PyTMX is efficient: +* Only the tiles used on a map are loaded into memory +* Map information is stored as integers, not python objects (32+kb) +* Extensive use of generators and iterators make it easy on memory +* Code is designed for compact size and readability + +### PyTMX is flexible: +* Supports all major Tiled features and object types +* PyTMX data classes can be extended +* Does not force you to render data in any particular way +* Includes many checks to give useful debugging information +* Supports pygame, pyglet, and pysdl2 image loading + +### PyTMX is supported: +* GitHub hosting allows for community participation +* I have kept PyTMX current with new versions of Tiled since v.7 + +### PyTMX is usable: +* Liberal LGPL license means you can use PyTMX for your project + + +Installation +=============================================================================== + +Install from pip + + pip install pytmx + + +You can also manually install it + + python setup.py install + + +Basic use: +=============================================================================== + +#### Just data, no images: + +```python +import pytmx +tiled_map = pytmx.TiledMap('map.tmx') +``` + +#### Load with pygame images: + +```python +from pytmx.util_pygame import load_pygame +tiled_map = load_pygame('map.tmx') +``` + +#### Load with pysdl2 images (experimental): + +```python +from pytmx.util_pysdl2 import load_pysdl2 +tiled_map = load_pysdl2('map.tmx') +``` + +#### Load with pyglet images (experimental): + +```python +from pytmx.util_pyglet import load_pyglet +tiled_map = load_pyglet('map.tmx') +``` + +#### Load from XML string: + +```python +import pytmx +tiled_map = pytmx.TiledMap.from_xml_string(some_string_here) +``` + +#### Iterate through layers and groups: + +```python +for layer in tiled_map.layers: + ... +``` + +#### Iterate through tile images in a tile layer: + +```python +for x, y, image in layer.tiles(): + ... +``` + +#### Iterate through Tiled objects in an object group: + +```python +for obj in layer: + ... +``` + +#### Get properties of various object types: + +```python + +# properties is a dict +TiledMap.properties +TiledTileLayer.properties['name'] +TiledObject.properties['type'] + +# tile ('GID') properties are accessed through the TiledMap: +properties = TiledMap.get_tile_properties(x, y, layer) +``` + +#### Get bounding box of an object: + +```python +bbox = obj.x, obj.y, obj.width, obj.height +``` + +#### Get the points/vertex to draw a polygon/polyline object: + +```python +points = obj.points +# if obj.closed == True, then obj is a polygon +``` + +Working with Maps +=============================================================================== + +TiledMap objects are returned from the loader. They contain layers, objects, +and a bunch of useful functions for getting information about the map. In +general, all of the pytmx types are not meant to be modified after being +returned from the loader. While there is a potentional for modifing them, +its not a supported function, and may change any time. Please consider them +read-only. + +Here is a list of attributes for use. (ie: TiledMap.layers): + +- layers: all layers in order +- tile_properties: dictionary of tile properties {GID: {props...}, ...} +- layernames: dictionary of layers with names: {name: layer, ...} +- images: list of all images in use, indexed by GID. Index 0 is always None. +- version +- orientation +- width: width of map in tiles, not pixels +- height: height of map in tiles, not pixels +- tileheight: height of tile in pixels. may differ between layers. +- tilewidth: width of tile in pixels. may differ between layers. +- background_color: map background color specified in Tiled +- properties: all user created properties about the map + + +#### Optional loading flags + +All loaders support the following flags: +- load_all_tiles: if True, all tiles will be loaded, even if unused +- invert_y: used for OpenGL graphics libs. Screen origin is at lower-left +- allow_duplicate_names: Force load maps with ambiguous data (see 'reserved names') + +```python +from pytmx.util_pygame import load_pygame +tiled_map = load_pygame(path_to_tmx_file, invert_y=True) +``` + +#### Loading from XML + +Most pytmx objects support loading from XML strings. For some objects, they require +references to other objects (like a layer has references to a tileset) and won't load +directly from XML. They can only be loaded if the entire map is loaded first. If you +want to store XML in a database or something, you can load the entire map with an XML string: + +```python +import pytmx +tiled_map = pytmx.TiledMap.from_xml_string(some_string_here) +``` + +#### Custom Image Loading + +The pytmx.TiledMap object constructor accepts an optional keyword "image_loader". The argument should be a function that accepts filename, colorkey (false, or a color) and pixelalpha (boolean) arguments. The function should return another function that will accept a rect-like object and any flags that the image loader might need to know, specific to the graphics library. Since that concept might be difficult to understand, I'll illustrate with some code. Use the following template code to load images from another graphics library. + + ```python +import pytmx + +def other_library_loader(filename, colorkey, **kwargs): + + # filename is a file to load an image from + # here you should load the image in whatever lib you want + + def extract_image(rect, flags): + + # rect is a (x, y, width, height) area where a particular tile is located + # flags is a named tuple that indicates how tile is flipped or rotated + + # use the rect to specify a region of the image file loaded in the function + # that encloses this one. + + # return an object to represent the tile + + # what is returned here will populate TiledMap.images, be returned by + # TiledObject.Image and included in TiledTileLayer.tiles() + + return extract_image + +level_map_and_images = pytmx.TiledMap("leveldata.tmx", image_loader=other_library_loader) +``` + +#### Accessing layers + +Layers are accessed through the TiledMap class and there are a few ways to get references to them: + +```python +# get a layer by name +layer_or_group = tiled_map.get_layer_by_name("base layer") + +# TiledMap.layers is a list of layers and groups +layer = tiled_map.layers[layer_index_number] + +# easily get references to just the visible tile layers +for layer in tiled_map.visible_tile_layers: + ... + +# just get references to visible object groups +for group in tile_map.visible_object_groups: + ... +``` + + +Working with Tile Layers +=============================================================================== + +Pytmx loads tile layers and their data: + +- name +- opacity +- visible: indicates if user has hidden the layer +- data: 2d array of all tile gids (normally not needed to use!) +- properties + +#### Tile Images + +Single tile images are accessible from TiledMap, TiledTileLayer, and TiledObject objects. +If you requre all images in a layer, there are more effecient ways described below. + +```python +# get image from the TiledMap using x, y, and layer numbers +pygame_surface = tile_map.get_tile_image(x, y, layer) + +# get tile image from an object with a image/GID assigned to it +image = obj.image + +# get image using gid (not needed for normal use!) +gid = layer.data[y][x] +image = tiled_map.images[gid] +``` + +#### Least effort involved getting all tile images + +```python +for x, y, image in layer.tiles(): + ... +``` + +#### Getting tile animations + +Tiled supports animated tiles, and pytmx has the ability to load them. +Animations are stored in the properties for the tile. The GID => image +conversion is already done for you. Animations from pytmx are a list +of AnimationFrame namedtuples. Please see the example below. + +```python +# just iterate over animated tiles and demo them + +# tmx_map is a TiledMap object +# tile_properties is a dictionary of all tile properties + +# iterate over the tile properties +for gid, props in tmx_map.tile_properties.items(): + + # iterate over the frames of the animation + for animation_frame in props['frames']: + + # do something with the image and duration of the frame + d = animation_frame.duration + i = animation_frame.image + ... + + # or just store the animation (list of frames) + my_anim = props['frames'] +``` + +#### If you really want to work with layer data directly... + +This information is provided for the curious, but for most people is not +required for normal use. + +Layer tiles are stored as a 'list of lists', or '2d array'. Each element of +layer data is a number which refers to a specific image in the map. These +numbers are called GID. Do not make references to these numbers, as they +will change if the map changes. + +Images for the GID can be accessed with the TiledMap.images list. + +With pygame, images will be plain pygame surfaces. These surfaces will be +checked for colorkey or per-pixel alpha automatically using information from +the TMX file and from checking each image for transparent pixels. You +do not need, and should not convert the tiles because it is already done. + +```python +layer = tiled_map.layers[0] +for x, y, gid in layer: + ... + +# get image using gid (not needed for normal use!) +# row index = 'y' +# column index = 'x' +image_gid = layer[row_index][column_index] +image = tiled_map.images[image_gid] + +# change GID of a position +layer[y][x] = new_gid +``` + +Working with Objects +=============================================================================== + +Tiled "objects" are things that are created in object layers, and include +polygons, polylings, boxes, ellispes, and tile objects. Pytmx loads all objects +and their data: + +- name +- type +- x +- y +- width +- height +- rotation +- gid (if it has an image) +- visible +- image +- properties + +#### Basics +Attributes x, y, width, and height all represent the bounding box of the object, +even polygons and polylines. + +#### Image Objects +If using a loader, then TiledObject.image will be a reference to the image used. + +#### Tile Objects +Tile Objects are objects that reference a tile in a tileset. These are loaded and +the image will be available to use. + +#### Polygon/Polyline Objects +These objects have special attributes: 'closed' and 'points'. Each point is (x, y) tuple. +If the object is a polygon, then TiledObject.closed will be True. Points are not +rotated if the rotation property is used. + +#### Accessing objects + +Objects can be accessed through the TiledMap or through a group. Object groups can be +used just like a python list, and support indexing, slicing, etc. + +```python +# search for an object with a specific name +my_object = tiled_map.get_object_by_name("baddy001") # will not return duplicates + +# get a group by name +group = tiled_map.get_layer_by_name("traps") + +# copy a group +traps = group[:] + +# iterate through objects in a group: +for obj in group: + ... +``` + +Understanding Properties +=============================================================================== + +Properties are a powerful feature of Tiled that allows the level designer to +assign key/value data to individual maps, tilesets, tiles, and objects. Pytmx +includes full support for reading this data so you can set parameters for stuff +in Tiled, instead of maintaining external data files, or even values in source. + +Properties are created by the user in tiled. There is also another set of data +that is part of each object, accessed by normal object attributes. This other +data is not set directly by the user, but is instead set by tiled. Typical +data that is object attributes are: 'name', 'x', 'opacity', or 'id'. + +If the user sets data for an object in Tiled, it becomes part of 'properties'. +'Properties' is just a normal python dictionary. + +```python +# get data normally set by Tiled +obj.name +obj.x +obj.opacity + +# get data set by the user in Tiled +obj.properties['hit points'] +obj.properties['goes to 11'] +``` + +Individual tile properties are accessed through the the parent map object: + +``` +tiled_map = TiledMap('level1.tmx') +props = tiled_map.get_tile_properties(x, y, layer) +props = tiled_map.get_tile_properties_by_gid(tile_gid) +``` + + +Scrolling Maps for Pygame +=============================================================================== + +I have another repo with a working demo of a proper scrolling map using Tiled +maps and pygame. Please feel free to test drive it. It isn't limited to Tiled +maps, you can use any data structure you want, as long as pygame is used. + +https://github.com/bitcraft/pyscroll + + +Reserved Names +================================================================================ + +Tiled supports user created metadata called "properties" for all the built-in +objects, like the map, tileset, objects, etc. Due to how the Tiled XML data is +stored, there are situations where Tiled internal metadata might have the same +name as user-created properties. + +Pytmx will raise a ValueError if it detects any conflicts. This check is +performed in order to prevent any situations where a level change might be made +in Tiled, but the programmer/designer doesn't know or forgets if the change was +made in the Tiled metadata or the user properties. + +I realize that it creates problems with certain common names like "id", or +"color". Overall, this check will help enforce clean design. + +However, If you really don't care about name conflicts, there is an option +you can try at your own risk. Pass 'allow_duplicate_names=True' to any +loader or to the TiledMap constructor and the checks will be disabled. + +```python +from pytmx.util_pygame import load_pygame +tiled_map = load_pygame('map.tmx', allow_duplicate_names=True) +``` + +In summary, don't use the following names when creating properties in Tiled: +As of 0.11.0, these values are: + +map: version, orientation, width, height, tilewidth, tileheight + properties, tileset, layer, objectgroup + +tileset: firstgid, source, name, tilewidth, tileheight, spacing, margin, + image, tile, properties + +tile: id, image, properties + +layer: name, x, y, width, height, opacity, properties, data + +objectgroup: name, color, x, y, width, height, opacity, object, properties + +object: id, name, type, x, y, width, height, gid, properties, polygon, + polyline, image + + +#### Please consider the following: + +PyTMX is a map __loader__. Pytmx takes the pain out of parsing XML, variable type conversion, shape loading, properties, and of course image loading. When asking for help, please understand that I want people to make their own games or utilities, and that PyTMX is able to make Tiled Maps easy to use. + +pytmx is not going to make your JRPG for you. You will need to do that yourself, and I, the author, cannot simply respond to every new developer who expects pytmx, pygame, or any other game library to simply make it work for them. Programming is a learned skill, and for most it takes practice and diligent study to get proficient at. I'm personally a nice guy, and do want to help, so before you flame me on your blog or reddit, understand what pytmx is used for, read the documentation and copy/paste the demo code if you have to. Thank you. + +I have a working solution to using Tiled Maps and Pygame ready for you. If you simply want a library to render the maps for you, please check it out, as they are designed to work together. + +http://github.com/bitcraft/pyscroll + + +Artwork Attributions +=============================================================================== +The 16x16 overworld tiles were created by MrBeast at opengameart.org. CC-BY 3.0 + +* If I missed your attribution, please let me know. + diff --git a/external/pytmx/util_pygame.py b/external/pytmx/util_pygame.py new file mode 100644 index 0000000..7744e80 --- /dev/null +++ b/external/pytmx/util_pygame.py @@ -0,0 +1,269 @@ +import logging +import itertools +import pytmx + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +try: + from pygame.transform import flip, rotate + import pygame +except ImportError: + logger.error('cannot import pygame (is it installed?)') + raise + +__all__ = ['load_pygame', 'pygame_image_loader', 'simplify', 'build_rects'] + + +def handle_transformation(tile, flags): + if flags.flipped_diagonally: + tile = flip(rotate(tile, 270), 1, 0) + if flags.flipped_horizontally or flags.flipped_vertically: + tile = flip(tile, flags.flipped_horizontally, flags.flipped_vertically) + return tile + + +def smart_convert(original, colorkey, pixelalpha): + """ + this method does several tests on a surface to determine the optimal + flags and pixel format for each tile surface. + + this is done for the best rendering speeds and removes the need to + convert() the images on your own + """ + tile_size = original.get_size() + threshold = 127 # the default + + # count the number of pixels in the tile that are not transparent + px = pygame.mask.from_surface(original, threshold).count() + + # there are no transparent pixels in the image + if px == tile_size[0] * tile_size[1]: + tile = original.convert() + + # there are transparent pixels, and tiled set a colorkey + elif colorkey: + tile = original.convert() + tile.set_colorkey(colorkey, pygame.RLEACCEL) + + # there are transparent pixels, and set for perpixel alpha + elif pixelalpha: + tile = original.convert_alpha() + + # there are transparent pixels, and we won't handle them + else: + tile = original.convert() + + return tile + + +def pygame_image_loader(filename, colorkey, **kwargs): + """ pytmx image loader for pygame + + :param filename: + :param colorkey: + :param kwargs: + :return: + """ + if colorkey: + colorkey = pygame.Color('#{0}'.format(colorkey)) + + pixelalpha = kwargs.get('pixelalpha', True) + image = pygame.image.load(filename) + + def load_image(rect=None, flags=None): + if rect: + try: + tile = image.subsurface(rect) + except ValueError: + logger.error('Tile bounds outside bounds of tileset image') + raise + else: + tile = image.copy() + + if flags: + tile = handle_transformation(tile, flags) + + tile = smart_convert(tile, colorkey, pixelalpha) + return tile + + return load_image + + +def load_pygame(filename, *args, **kwargs): + """ Load a TMX file, images, and return a TiledMap class + + PYGAME USERS: Use me. + + this utility has 'smart' tile loading. by default any tile without + transparent pixels will be loaded for quick blitting. if the tile has + transparent pixels, then it will be loaded with per-pixel alpha. this is + a per-tile, per-image check. + + if a color key is specified as an argument, or in the tmx data, the + per-pixel alpha will not be used at all. if the tileset's image has colorkey + transparency set in Tiled, the util_pygam will return images that have their + transparency already set. + + TL;DR: + Don't attempt to convert() or convert_alpha() the individual tiles. It is + already done for you. + """ + kwargs['image_loader'] = pygame_image_loader + return pytmx.TiledMap(filename, *args, **kwargs) + + +def build_rects(tmxmap, layer, tileset=None, real_gid=None): + """generate a set of non-overlapping rects that represents the distribution + of the specified gid. + + useful for generating rects for use in collision detection + + Use at your own risk: this is experimental...will change in future + + GID Note: You will need to add 1 to the GID reported by Tiled. + + :param tmxmap: TiledMap object + :param layer: int or string name of layer + :param tileset: int or string name of tileset + :param real_gid: Tiled GID of the tile + 1 (see note) + :return: List of pygame Rect objects + """ + if isinstance(tileset, int): + try: + tileset = tmxmap.tilesets[tileset] + except IndexError: + msg = "Tileset #{0} not found in map {1}." + logger.debug(msg.format(tileset, tmxmap)) + raise IndexError + + elif isinstance(tileset, str): + try: + tileset = [t for t in tmxmap.tilesets if t.name == tileset].pop() + except IndexError: + msg = "Tileset \"{0}\" not found in map {1}." + logger.debug(msg.format(tileset, tmxmap)) + raise ValueError + + elif tileset: + msg = "Tileset must be either a int or string. got: {0}" + logger.debug(msg.format(type(tileset))) + raise TypeError + + gid = None + if real_gid: + try: + gid, flags = tmxmap.map_gid(real_gid)[0] + except IndexError: + msg = "GID #{0} not found" + logger.debug(msg.format(real_gid)) + raise ValueError + + if isinstance(layer, int): + layer_data = tmxmap.get_layer_data(layer) + elif isinstance(layer, str): + try: + layer = [l for l in tmxmap.layers if l.name == layer].pop() + layer_data = layer.data + except IndexError: + msg = "Layer \"{0}\" not found in map {1}." + logger.debug(msg.format(layer, tmxmap)) + raise ValueError + + p = itertools.product(range(tmxmap.width), range(tmxmap.height)) + if gid: + points = [(x, y) for (x, y) in p if layer_data[y][x] == gid] + else: + points = [(x, y) for (x, y) in p if layer_data[y][x]] + + rects = simplify(points, tmxmap.tilewidth, tmxmap.tileheight) + return rects + + +def simplify(all_points, tilewidth, tileheight): + """Given a list of points, return list of rects that represent them + kludge: + + "A kludge (or kluge) is a workaround, a quick-and-dirty solution, + a clumsy or inelegant, yet effective, solution to a problem, typically + using parts that are cobbled together." + + -- wikipedia + + turn a list of points into a rects + adjacent rects will be combined. + + plain english: + the input list must be a list of tuples that represent + the areas to be combined into rects + the rects will be blended together over solid groups + + so if data is something like: + + 0 1 1 1 0 0 0 + 0 1 1 0 0 0 0 + 0 0 0 0 0 4 0 + 0 0 0 0 0 4 0 + 0 0 0 0 0 0 0 + 0 0 1 1 1 1 1 + + you'll have the 4 rects that mask the area like this: + + ..######...... + ..####........ + ..........##.. + ..........##.. + .............. + ....########## + + pretty cool, right? + + there may be cases where the number of rectangles is not as low as possible, + but I haven't found that it is excessively bad. certainly much better than + making a list of rects, one for each tile on the map! + """ + def pick_rect(points, rects): + ox, oy = sorted([(sum(p), p) for p in points])[0][1] + x = ox + y = oy + ex = None + + while 1: + x += 1 + if not (x, y) in points: + if ex is None: + ex = x - 1 + + if (ox, y + 1) in points: + if x == ex + 1: + y += 1 + x = ox + + else: + y -= 1 + break + else: + if x <= ex: y -= 1 + break + + c_rect = pygame.Rect(ox * tilewidth, oy * tileheight, + (ex - ox + 1) * tilewidth, + (y - oy + 1) * tileheight) + + rects.append(c_rect) + + rect = pygame.Rect(ox, oy, ex - ox + 1, y - oy + 1) + kill = [p for p in points if rect.collidepoint(p)] + [points.remove(i) for i in kill] + + if points: + pick_rect(points, rects) + + rect_list = [] + while all_points: + pick_rect(all_points, rect_list) + + return rect_list diff --git a/external/pytmx/util_pyglet.py b/external/pytmx/util_pyglet.py new file mode 100644 index 0000000..6b4a38a --- /dev/null +++ b/external/pytmx/util_pyglet.py @@ -0,0 +1,59 @@ +import logging + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +try: + import pyglet +except ImportError: + logger.error('cannot import pyglet (is it installed?)') + raise + +import pytmx + + +def pyglet_image_loader(filename, colorkey, **kwargs): + """basic image loading with pyglet + + returns pyglet Images, not textures + + This is a basic proof-of-concept and is likely to fail in some situations. + + Missing: + Transparency + Tile Rotation + + This is slow as well. + """ + if colorkey: + logger.debug('colorkey not implemented') + + image = pyglet.image.load(filename) + + def load_image(rect=None, flags=None): + if rect: + try: + x, y, w, h = rect + y = image.height - y - h + tile = image.get_region(x, y, w, h) + except: + logger.error('cannot get region %s of image', rect) + raise + else: + tile = image + + if flags: + logger.error('tile flags are not implemented') + + return tile + + return load_image + + +def load_pyglet(filename, *args, **kwargs): + kwargs['image_loader'] = pyglet_image_loader + kwargs['invert_y'] = True + return pytmx.TiledMap(filename, *args, **kwargs) diff --git a/external/pytmx/util_pysdl2.py b/external/pytmx/util_pysdl2.py new file mode 100644 index 0000000..a88793a --- /dev/null +++ b/external/pytmx/util_pysdl2.py @@ -0,0 +1,66 @@ +import logging +from functools import partial + +logger = logging.getLogger(__name__) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +logger.addHandler(ch) +logger.setLevel(logging.INFO) + +try: + import sdl2.ext +except ImportError: + logger.error('cannot import pysdl2 (is it installed?)') + raise + +import pytmx + +__all__ = ['load_pysdl2', 'pysdl2_image_loader', ] +flag_names = ( + 'flipped_horizontally', + 'flipped_vertically', + 'flipped_diagonally',) + + +def pysdl2_image_loader(renderer, filename, colorkey, **kwargs): + def convert(surface): + texture_ = sdl2.SDL_CreateTextureFromSurface(renderer.renderer, surface) + sdl2.SDL_SetTextureBlendMode(texture_, sdl2.SDL_BLENDMODE_BLEND) + sdl2.SDL_FreeSurface(surface) + return texture_ + + def load_image(rect=None, flags=None): + if rect: + try: + flip = 0 + if flags.flipped_horizontally: + flip |= sdl2.SDL_FLIP_HORIZONTAL + if flags.flipped_vertically: + flip |= sdl2.SDL_FLIP_VERTICAL + if flags.flipped_diagonally: + flip |= 4 + + rect = sdl2.rect.SDL_Rect(*rect) + return texture, rect, flip + + except ValueError: + logger.error('Tile bounds outside bounds of tileset image') + raise + else: + return texture, None, 0 + + image = sdl2.ext.load_image(filename) + + if colorkey: + colorkey = sdl2.ext.string_to_color('#' + colorkey) + key = sdl2.SDL_MapRGB(image.format, *colorkey[:3]) + sdl2.SDL_SetColorKey(image, sdl2.SDL_TRUE, key) + + texture = convert(image) + + return load_image + + +def load_pysdl2(renderer, filename, *args, **kwargs): + kwargs['image_loader'] = partial(pysdl2_image_loader, renderer) + return pytmx.TiledMap(filename, *args, **kwargs) diff --git a/external/six.py b/external/six.py new file mode 100644 index 0000000..190c023 --- /dev/null +++ b/external/six.py @@ -0,0 +1,868 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2015 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson <benjamin@python.org>" +__version__ = "1.10.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + if from_value is None: + raise value + raise value from from_value +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + raise value from from_value +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/external/six.pyc b/external/six.pyc Binary files differnew file mode 100644 index 0000000..ddff77d --- /dev/null +++ b/external/six.pyc diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/gui/__init__.py diff --git a/gui/__init__.pyc b/gui/__init__.pyc Binary files differnew file mode 100644 index 0000000..de4ecef --- /dev/null +++ b/gui/__init__.pyc diff --git a/gui/__pycache__/__init__.cpython-37.pyc b/gui/__pycache__/__init__.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..babd95b --- /dev/null +++ b/gui/__pycache__/__init__.cpython-37.pyc diff --git a/gui/__pycache__/managui.cpython-37.pyc b/gui/__pycache__/managui.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..c15bd16 --- /dev/null +++ b/gui/__pycache__/managui.cpython-37.pyc diff --git a/gui/chatlog.py b/gui/chatlog.py new file mode 100644 index 0000000..aca9564 --- /dev/null +++ b/gui/chatlog.py @@ -0,0 +1,86 @@ +from kivy.event import EventDispatcher +from kivy.uix.abstractview import AbstractView +from kivy.properties import ListProperty, NumericProperty +from kivy.adapters.simplelistadapter import SimpleListAdapter +from kivy.utils import escape_markup +from kivy.clock import Clock +from kivy.uix.label import Label +# from utils import log_method + +from textutils import (links_to_markup, replace_emotes, preprocess, + remove_formatting) + + +class ChatLog(AbstractView, EventDispatcher): + + def __init__(self, **kwargs): + # Check for adapter argument + if 'adapter' not in kwargs: + list_adapter = SimpleListAdapter(data=[], cls=Label) + kwargs['adapter'] = list_adapter + + super(ChatLog, self).__init__(**kwargs) + + self._views = [] + + populate = self._trigger_populate = Clock.create_trigger( + self._populate, -1) + + fbind = self.fbind + fbind('adapter', populate) + fbind('size', populate) + fbind('pos', populate) + + max_lines = NumericProperty(100) + cut_lines = NumericProperty(10) + + def _populate(self, *args): + container = self.container + adapter = self.adapter + container.clear_widgets() + self._views = [] + + for index in range(adapter.get_count()): + item_view = adapter.get_view(index) + self.container.add_widget(item_view) + self._views.append(item_view) + + container.height = self._container_height() + + def _append(self, msg): + container = self.container + adapter = self.adapter + views = self._views + cl = self.cut_lines + + if len(views) >= self.max_lines: + container.clear_widgets(views[:cl]) + self._views = views[cl:] + adapter.data = adapter.data[cl:] + + adapter.data.append(msg) + item_view = adapter.get_view(adapter.get_count() - 1) + item_view.texture_update() + + self._views.append(item_view) + container.add_widget(item_view) + + def _container_height(self, *args): + h = 0 + for v in self._views: + h += v.height + return h + + def append_message(self, msg): + msg = preprocess(msg, (replace_emotes, + remove_formatting)) + msg = links_to_markup(escape_markup(msg)) + self._append(msg) + self.container.height = self._container_height() + self.children[0].scroll_y = 0 + + def msg_converter(self, index, msg): + b = (index % 2) * 0.04 + return {'text': msg, + 'width': self.width, + 'background_color': (0 + b, 0.17 + b, 0.21 + b, 1)} diff --git a/gui/handlers.py b/gui/handlers.py new file mode 100644 index 0000000..b6f9d0b --- /dev/null +++ b/gui/handlers.py @@ -0,0 +1,49 @@ + +import net.mapserv as mapserv +from loggers import debuglog +from utils import extends + + +__all__ = ['app'] + + +map_name = "" +app = None + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + if data.code == 0: + last_nick = mapserv.last_whisper['to'] + app.root.chat_input.text = '/w "{}" '.format(last_nick) + app.root.chat_input.focus = True + + +@extends('smsg_player_warp') +def player_warp(data): + mapserv.cmsg_map_loaded() + m = "[warp] {} ({},{})".format(data.map, data.x, data.y) + debuglog.info(m) + + +@extends('smsg_char_map_info') +def char_map_info(data): + global map_name + map_name = data.map_name + + +@extends('smsg_map_login_success') +def map_login_success(data): + m = "[map] {} ({},{})".format(map_name, data.coor.x, data.coor.y) + debuglog.info(m) + mapserv.server.raw = True + mapserv.cmsg_map_loaded() + + +@extends('smsg_connection_problem') +def connection_problem(data): + error_codes = { + 2 : "Account already in use" + } + msg = error_codes.get(data.code, str(data.code)) + debuglog.error('Connection problem: %s', msg) diff --git a/gui/handlers.pyc b/gui/handlers.pyc Binary files differnew file mode 100644 index 0000000..8532460 --- /dev/null +++ b/gui/handlers.pyc diff --git a/gui/icon.png b/gui/icon.png Binary files differnew file mode 100644 index 0000000..3bea05d --- /dev/null +++ b/gui/icon.png diff --git a/gui/managui.kv b/gui/managui.kv new file mode 100644 index 0000000..7e41701 --- /dev/null +++ b/gui/managui.kv @@ -0,0 +1,155 @@ +#:import la kivy.adapters.simplelistadapter +#:import ChatLog gui.chatlog.ChatLog +#:import PlayersList gui.plist.PlayersList +#:import PlayersListItem gui.plist.PlayersListItem + + +<PlayersListItem>: + + canvas.before: + Color: + rgba: 0.05, 0.05, 0.05, 1 + Line: + points: [ self.x, self.y, self.x+self.width, self.y ] + + Label: + text: root.nick + + +<PlayersList>: + + canvas.before: + Color: + rgba: .25, .1, .9, 1 + Rectangle: + pos: self.pos + size: self.size + + +<RootWidget>: + mobile: True if self.width < dp(600) else False + + canvas.before: + Color: + rgba: 0.01, 0.33, 0.32, 1 + Rectangle: + pos: self.pos + size: self.size + + messages_log: id_chat_log + chat_input: id_chat_input + players_list: id_players_list + + BoxLayout: + orientation: 'horizontal' + + BoxLayout: + orientation: 'vertical' + + ChatLog: + id: id_chat_log + adapter: + la.SimpleListAdapter( + data=['Welcome to [ref=https://bitbucket.org/rumly111/manachat/][color=0000ff]ManaChat[/color][/ref]. Press F1 to show settings. Press ESCAPE to toggle menu.'], + template='ChatLogItem', + args_converter=self.msg_converter) + + TextInput: + id: id_chat_input + size_hint_y: None + height: '50dp' + # focus: True + multiline: False + on_text_validate: root.on_command_enter(args) + + PlayersList: + id: id_players_list + size_hint_x: None + width: '150dp' + + +[ChatLogItem@Label]: + + canvas.before: + Color: + rgba: ctx.background_color + Rectangle: + pos: self.pos + size: self.size + + text: ctx.text + width: ctx.width + text_size: self.width, None + size_hint: None, None + height: self.texture_size[1] + 10 + markup: True + on_ref_press: app.open_link(args[1]) + + +<ChatLog>: + container: container + ScrollView: + pos: root.pos + do_scroll_x: False + GridLayout: + cols: 1 + id: container + size_hint_y: None + + +<AboutPopup@Popup>: + title_size: '14dp' + title: 'About' + size_hint: 0.9, None + height: '140dp' + + Label: + id: lbl + text_size: self.width, None + size_hint_y: None + height: self.texture_size[1] + text: + '''ManaChat is a multi-purpose chat client for The Mana World MMORPG + Author: Joseph Botosh <rumly111@gmail.com> (TMW nickname: Travolta) + Licence: Gnu General Public Licence, rev. 2 + Homepage: [ref=https://bitbucket.org/rumly111/manachat/]https://bitbucket.org/rumly111/manachat/[/ref]''' + + markup: True + on_ref_press: app.open_link(args[1]) + + +<MenuPopup@Popup>: + title_size: '14dp' + title: 'ManaChat' + auto_dismiss: False + size_hint: None, None + width: "200dp" + height: "250dp" + + BoxLayout: + spacing: "2dp" + padding: "2dp" + orientation: "vertical" + + Button: + text: "Connect" + on_press: app.reconnect() + + Button: + text: "Config" + on_press: app.open_settings() + + Button: + text: "About" + on_press: app.show_about() + + Button: + text: "Exit" + on_press: app.stop() + + +<SettingPassword>: + Label: + text: '*' * len(root.value) if root.value else '' + pos: root.pos + font_size: '15sp' diff --git a/gui/managui.py b/gui/managui.py new file mode 100644 index 0000000..d6dd79a --- /dev/null +++ b/gui/managui.py @@ -0,0 +1,192 @@ +#!/usr/bin/python2 +# -- coding: utf-8 -- + +import asyncore +import logging +import webbrowser + +import kivy +kivy.require('1.9.1') + +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.properties import BooleanProperty +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.popup import Popup + +from kivy.config import ConfigParser +config = ConfigParser() +config.read("manachat.ini") + +import monsterdb +import itemdb +import net +import net.mapserv as mapserv +import gui.handlers as handlers +import plugins +from commands import process_line +from net.onlineusers import OnlineUsers +from loggers import netlog, debuglog +from logicmanager import logic_manager + + +class DebugLogHandler(logging.Handler): + + def __init__(self, app, **kwargs): + self.app = app + super(self.__class__, self).__init__(**kwargs) + + def emit(self, record): + msg = self.format(record) + self.app.root.messages_log.append_message(msg) + + +class MenuPopup(Popup): + visible = BooleanProperty(False) + + def on_open(self, *args): + self.visible = True + + def on_dismiss(self, *args): + self.visible = False + + +class AboutPopup(Popup): + pass + + +class RootWidget(FloatLayout): + mobile = BooleanProperty(False) + + def _focus_chat_input(self, dt): + self.chat_input.focus = True + + def on_command_enter(self, *args): + process_line(self.chat_input.text.encode('utf-8')) + self.chat_input.text = '' + Clock.schedule_once(self._focus_chat_input, 0.1) # dirty hack :^) + + +class ManaGuiApp(App): + use_kivy_settings = BooleanProperty(False) + + def hook_keyboard(self, window, key, *largs): + if key == 27: + self.show_menu(not self._menu_popup.visible) + # self.stop() + return True + return False + + def on_start(self): + if config.getboolean('Other', 'log_network_packets'): + import os + import tempfile + + logfile = os.path.join(tempfile.gettempdir(), "netlog.txt") + netlog.setLevel(logging.INFO) + fh = logging.FileHandler(logfile, mode="w") + fmt = logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S") + fh.setFormatter(fmt) + netlog.addHandler(fh) + + monsterdb.read_monster_db() + itemdb.load_itemdb('itemdb.txt') + + dbgh = DebugLogHandler(self) + dbgh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%H:%M")) + debuglog.addHandler(dbgh) + debuglog.setLevel(logging.INFO) + + net2 = DebugLogHandler(self) + net2.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%H:%M")) + net2.setLevel(logging.ERROR) + netlog.addHandler(net2) + + plugins.load_plugins(config) + + handlers.app = self + + import chat + chat.pp_actions = () + + # self.reconnect() + + Clock.schedule_once(self.update_online_list, 0.2) + Clock.schedule_interval(self.update_online_list, 35) + Clock.schedule_interval(self.update_loop, 0) + Clock.schedule_once(self.show_menu, 1.5) + + def build(self): + self.icon = 'icon.png' + Window.bind(on_keyboard=self.hook_keyboard) + return RootWidget() + + def build_settings(self, settings): + from kivy.uix.settings import SettingString + + class SettingPassword(SettingString): + def _create_popup(self, instance): + SettingString._create_popup(self, instance) + self.textinput.password = True + + def add_widget(self, *largs): + if self.content is not None: + self.content.clear_widgets() + return SettingString.add_widget(self, *largs) + + settings.register_type('password', SettingPassword) + settings.add_json_panel('ManaChat', config, + filename='manachat.json') + + def update_loop(self, *l): + asyncore.loop(timeout=0, count=10) + logic_manager.tick() + + def on_pause(self): + return True + + def on_stop(self): + Clock.unschedule(self.update_loop) + Clock.unschedule(self.update_online_list) + + def open_link(self, link): + webbrowser.open(link) + + def update_online_list(self, *l): + lst = OnlineUsers.dl_online_list(config.get('Other', + 'online_txt_url')) + if lst is not None: + lst.sort() + self.root.players_list.items = lst + + def reconnect(self): + if mapserv.server is not None: + mapserv.cleanup() + + net.login(host=config.get('Server', 'host'), + port=config.getint('Server', 'port'), + username=config.get('Player', 'username'), + password=config.get('Player', 'password'), + charname=config.get('Player', 'charname')) + + if hasattr(self, '_menu_popup'): + self._menu_popup.dismiss() + + def show_about(self): + AboutPopup().open() + + def show_menu(self, show=True): + if not hasattr(self, '_menu_popup'): + self._menu_popup = MenuPopup() + if show: + self._menu_popup.open() + else: + self._menu_popup.dismiss() + + +if __name__ == "__main__": + ManaGuiApp().run() diff --git a/gui/managui.pyc b/gui/managui.pyc Binary files differnew file mode 100644 index 0000000..74b595d --- /dev/null +++ b/gui/managui.pyc diff --git a/gui/pathfind.py b/gui/pathfind.py new file mode 100644 index 0000000..2c870e1 --- /dev/null +++ b/gui/pathfind.py @@ -0,0 +1,91 @@ +import collections + + +class Queue: + def __init__(self): + self.elements = collections.deque() + + def empty(self): + return len(self.elements) == 0 + + def put(self, x): + self.elements.append(x) + + def get(self): + return self.elements.popleft() + + +class Graph: + def __init__(self, collisions): + self.collisions = collisions + self.width = len(collisions[0]) + self.height = len(collisions) + + def walkable(self, coor): + x, y = coor + if x < 0 or x > self.width - 1: + return False + if y < 0 or y > self.height - 1: + return False + if self.collisions[self.height - y - 1][x] > 0: + return False + return True + + def neighbors(self, coor): + n = [] + x, y = coor + + for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): + nx = x + dx + ny = y + dy + if self.walkable((nx, ny)): + n.append((nx, ny)) + + for dx, dy in ((-1, -1), (-1, 1), (1, -1), (1, 1)): + nx = x + dx + ny = y + dy + if (self.walkable((nx, ny)) and + self.walkable((nx, y)) and + self.walkable((x, ny))): + n.append((nx, ny)) + + return n + + +def breadth_first_search(collisions, start, goal): + graph = Graph(collisions) + frontier = Queue() + frontier.put(start) + came_from = {} + came_from[start] = None + + while not frontier.empty(): + current = frontier.get() + + if current == goal: + break + + for next in graph.neighbors(current): + if next not in came_from: + frontier.put(next) + came_from[next] = current + + current = goal + path = [current] + while current != start: + current = came_from[current] + path.append(current) + path.reverse() + + # simplify path + spath = path[:1] + for i in range(1, len(path) - 1): + px, py = path[i - 1] + cx, cy = path[i] + nx, ny = path[i + 1] + if nx - cx == cx - px and ny - cy == cy - py: + continue + spath.append(path[i]) + spath.append(path[-1]) + + return spath diff --git a/gui/plist.py b/gui/plist.py new file mode 100644 index 0000000..a009a32 --- /dev/null +++ b/gui/plist.py @@ -0,0 +1,54 @@ +from kivy.app import App +from kivy.adapters.listadapter import ListAdapter +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout +from kivy.properties import StringProperty, ListProperty +from kivy.uix.listview import ListView, ListItemLabel + + +class PlayersListItem(BoxLayout, ListItemLabel): + nick = StringProperty(allow_none=False) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + return False + touch.grab(self) + return True + + def on_touch_up(self, touch): + if not self.collide_point(*touch.pos): + return False + if touch.grab_current is self: + app = App.get_running_app() + app.root.chat_input.text = '/w "{}" '.format(self.nick) + app.root.chat_input.focus = True + touch.ungrab(self) + return True + + +class PlayersList(FloatLayout): + items = ListProperty([]) + + def __init__(self, **kwargs): + super(PlayersList, self).__init__(**kwargs) + + player_args_coverter = lambda row_index, nick: {"nick" : nick, + "size_hint_y": None, "height": "30dp", "pos_hint_y": 0.9 } + + list_adapter = ListAdapter( + data=["Ginaria", "Celestia"], + args_converter=player_args_coverter, + selection_mode='single', + allow_empty_selection=True, + cls=PlayersListItem) + + list_view = ListView(adapter=list_adapter, + size_hint=(1.0, 1.0), + pos_hint={'center_x': 0.5}) + + def data_changed(instance, value): + list_adapter.data = value + + self.bind(items=data_changed) + + self.add_widget(list_view) diff --git a/gui/tmxmap.py b/gui/tmxmap.py new file mode 100644 index 0000000..800868e --- /dev/null +++ b/gui/tmxmap.py @@ -0,0 +1,175 @@ + +# import pytmx +import zipfile +from itertools import cycle, islice +try: + import cPickle as pickle +except ImportError: + import pickle + +from kivy.core.image import Image as CoreImage +from kivy.uix.widget import Widget +from kivy.uix.image import Image +from kivy.graphics.texture import Texture +from kivy.animation import Animation +from kivy.properties import (StringProperty, ObjectProperty, + NumericProperty, DictProperty) +from kivy.logger import Logger + +from pathfind import breadth_first_search +import net.mapserv as mapserv + + +def kivy_image_loader(filename, colorkey, **kwargs): + texture = CoreImage(filename).texture + + def loader(rect, flags): + x, y, w, h = rect + t = texture.get_region(x, + texture.height - y - h, + w, h) + + if flags.flipped_diagonally: + t.flip_horizontal() + t.flip_vertical() + elif flags.flipped_vertically: + t.flip_vertical() + elif flags.flipped_horizontally: + t.flip_horizontal() + + return t + + return loader + + +def create_map_texture(map_data, tile_size=4): + map_size = len(map_data[0]), len(map_data) + texture = Texture.create(size=(map_size[0] * tile_size, + map_size[1] * tile_size), + colorfmt='rgb') + + c = cycle('\x45\x46\x47') + s = islice(c, 3 * 4000) + buf = b''.join(s) + + for y, row in enumerate(map_data): + for x, cell in enumerate(row): + if cell > 0: + continue + + texture.blit_buffer(buf, size=(tile_size, tile_size), + colorfmt='rgb', + pos=(x * tile_size, y * tile_size)) + + texture.flip_vertical() + + return texture + + +class BeingWidget(Widget): + anim = ObjectProperty(None) + name = StringProperty() + + +class MapWidget(Image): + tile_size = NumericProperty(32) + player = ObjectProperty() + collisions = ObjectProperty(None) + current_attacks = DictProperty() + beings = DictProperty() + maps = {} + + def load_map(self, name, *args): + if name not in self.maps: + Logger.info("Caching map %s", name) + zf = zipfile.ZipFile('mapdb.zip', 'r') + self.maps[name] = pickle.load(zf.open(name + '.pickle')) + zf.close() + # m = pytmx.TiledMap(filename=filename) + # data = m.get_layer_by_name('Collision').data + texture = create_map_texture(self.maps[name]['collisions'], + self.tile_size) + self.texture = texture + self.size = texture.size + self.collisions = self.maps[name]['collisions'] + + def to_game_coords(self, pos): + ts = self.tile_size + height = len(self.collisions) + x = int(pos[0] // ts) + y = height - int(pos[1] // ts) - 1 + return x, y + + def from_game_coords(self, pos): + ts = self.tile_size + height = len(self.collisions) + x = pos[0] * ts + y = (height - pos[1] - 1) * ts + return x, y + + def move_being(self, being, gx, gy, speed=150): + ts = self.tile_size + ox = int(being.x // ts) + oy = int(being.y // ts) + gy = len(self.collisions) - gy - 1 + + try: + path = breadth_first_search(self.collisions, + (ox, oy), (gx, gy)) + except KeyError: + path = [] + + if being.anim: + being.anim.stop(being) + + being.anim = Animation(x=being.x, y=being.y, duration=0) + + for i in range(len(path) - 1): + cx, cy = path[i + 1] + px, py = path[i] + distance = max(abs(cx - px), abs(cy - py)) + being.anim += Animation(x=cx * ts, y=cy * ts, + duration=distance * speed / 1000.) + + being.anim.start(being) + + def get_attack_points(self, *bind_args): + points = [] + try: + player_id = mapserv.server.char_id + except AttributeError: + player_id = 0 + for (id1, id2) in self.current_attacks: + try: + # Temporary workaround + if id1 == player_id: + x1, y1 = self.player.pos + else: + x1, y1 = self.beings[id1].pos + if id2 == player_id: + x2, y2 = self.player.pos + else: + x2, y2 = self.beings[id2].pos + points.extend([x1, y1, x2, y2]) + except: + pass + + return points + + def get_attack_endpoints(self, *bind_args): + points = [] + try: + player_id = mapserv.server.char_id + except AttributeError: + player_id = 0 + for (_, target) in self.current_attacks: + try: + if target == player_id: + x, y = self.player.pos + else: + x, y = self.beings[target].pos + points.extend([x, y]) + except: + pass + + return points diff --git a/headless.py b/headless.py new file mode 100755 index 0000000..59d17c6 --- /dev/null +++ b/headless.py @@ -0,0 +1,57 @@ +#!/usr/bin/python2 + +import os +import sys +import asyncore +import logging +from ConfigParser import ConfigParser + +try: + import construct + del construct +except ImportError: + sys.path.insert(0, os.path.join(os.getcwd(), "external")) + +import net +import net.mapserv as mapserv +import plugins +from utils import extends +from itemdb import load_itemdb +from logicmanager import logic_manager + + +@extends('smsg_player_warp') +def player_warp(data): + mapserv.cmsg_map_loaded() + + +@extends('smsg_map_login_success') +def map_login_success(data): + mapserv.cmsg_map_loaded() + + +if __name__ == '__main__': + logging.basicConfig(format="[%(asctime)s] %(message)s", + level=logging.INFO, + datefmt="%Y-%m-%d %H:%M:%S") + + config_ini = 'manachat.ini' + if len(sys.argv) > 1: + if sys.argv[1].endswith('.ini') and os.path.isfile(sys.argv[1]): + config_ini = sys.argv[1] + config = ConfigParser() + config.read(config_ini) + + load_itemdb('itemdb.txt') + + plugins.load_plugins(config) + + net.login(host=config.get('Server', 'host'), + port=config.getint('Server', 'port'), + username=config.get('Player', 'username'), + password=config.get('Player', 'password'), + charname=config.get('Player', 'charname')) + + while True: + asyncore.loop(timeout=0.2, count=5) + logic_manager.tick() diff --git a/icon.png b/icon.png Binary files differnew file mode 100644 index 0000000..3bea05d --- /dev/null +++ b/icon.png diff --git a/itemdb.py b/itemdb.py new file mode 100644 index 0000000..34bde26 --- /dev/null +++ b/itemdb.py @@ -0,0 +1,27 @@ + +from loggers import debuglog + +item_names = {0: 'GP'} + + +def load_itemdb(dbfile='itemdb.txt'): + with open(dbfile, 'rt') as f: + for l in f.readlines(): + try: + sn, sr = l.split(' ', 1) + item_id = int(sn) + item_name = sr[:-1] if sr.endswith('\n') else sr + item_names[item_id] = item_name + except ValueError: + pass + + debuglog.info("Loaded itemdb from %s", dbfile) + return item_names + + +def item_name(item_id, mplus=False): + name = item_names.get(item_id, 'Item' + str(item_id)) + if mplus: + return '[@@{}|{}@@]'.format(item_id, name) + else: + return name diff --git a/itemdb.pyc b/itemdb.pyc Binary files differnew file mode 100644 index 0000000..8c69172 --- /dev/null +++ b/itemdb.pyc diff --git a/itemdb.txt b/itemdb.txt new file mode 100644 index 0000000..c89df34 --- /dev/null +++ b/itemdb.txt @@ -0,0 +1,880 @@ +501 Cactus Drink +502 Cactus Potion +506 Candy Cane +508 Xmas Cake +509 Chocolate Bar +510 Candy +512 Ginger Bread Man +513 Cake +514 Xmas Candy Cane +519 Cherry Cake +520 Easter Egg +527 Milk +533 Roasted Maggot +534 Orange Cupcake +535 Red Apple +539 Beer +541 Bottle of Water +562 Chicken Leg +565 Pink Petal +566 Small Mushroom +567 Iron Potion +568 Concentration Potion +657 Orange +676 Steak +684 Tiny Healing Potion +685 Small Healing Potion +686 Medium Healing Potion +687 Large Healing Potion +705 Mana Potion +714 Snake Egg +715 Mountain Snake Egg +716 Grass Snake Egg +717 Cave Snake Egg +719 Green Apple +733 Purification Potion +736 White Cake +737 Chocolate Cake +738 Orange Cake +739 Apple Cake +743 Acorn +744 Diluted Concentration Potion +745 Dark Concentration Potion +746 Mopox Cure Potion +747 Laced Chocolate Cake +748 Laced Orange Cupcake +750 Slow Poison Potion +784 Zombie Nachos +785 Lady Fingers +786 Slime Jell-Ahh +787 Snapple +788 Beetle Juice +789 Gut Buster Ale +790 Blood Wine +808 Hitchhiker's Towel +809 White Hitchhiker's Towel +810 Red Hitchhiker's Towel +811 Green Hitchhiker's Towel +812 Blue Hitchhiker's Towel +813 Yellow Hitchhiker's Towel +814 Purple Hitchhiker's Towel +815 Orange Hitchhiker's Towel +816 Pink Hitchhiker's Towel +817 Teal Hitchhiker's Towel +818 Lime Hitchhiker's Towel +825 Tiny Mana Elixir +826 Small Mana Elixir +827 Medium Mana Elixir +828 Large Mana Elixir +838 Cranberry Lollipop +839 Grape Lollipop +840 Orange Lollipop +1189 Pollett Egg +1229 Caramel Apple +1230 Booberry Blue Lollipop +1231 Ghastly Green Lollipop +1232 Blood Red Lollipop +1248 Blueberries +1250 Pear +1251 Plum +1252 Cherry +1253 Golden Delicious Apple +1258 Honey +1280 Scissors +1281 Shock Sweet +3001 Rubber Bat +3006 Tonori Delight +3007 Marshmallow +3009 Jelly Skull +3010 Candy Pumpkin +4035 Pickled Beets +4036 Roasted Acorn +503 Casino Coins +504 Decor Candy +505 Maggot Slime +507 Scorpion Stinger +515 Purple Present Box +516 Blue Present Box +517 Red Scorpion Stinger +518 Bug Leg +526 Coin Bag +537 Treasure Key +538 Green Present Box +540 Empty Bottle +542 Bottle of Sand +551 Aqua Hint +552 Magenta Hint +553 Yellow Hint +554 Green Hint +555 Teal Hint +556 Purple Hint +557 Red Hint +558 Blue Hint +559 Orange Hint +560 Gray Hint +569 Raw Log +589 Toothbrush +611 White Fur +612 Cave Snake Lamp +613 Hard Spike +614 Pink Antenna +631 Dark Crystal +635 Santa Cookie +640 Iron Ore +641 Snake Skin +660 Cotton Cloth +661 Red Rose +662 White Rose +663 Dark Red Rose +664 Pink Rose +665 Yellow Rose +666 Black Rose +667 Orange Rose +668 Blue Rose +669 Yellow Tulip +670 Purple Tulip +671 Red Tulip +672 White Tulip +673 Pink Tulip +674 Orange Tulip +680 Mauve Herb +681 Cobalt Herb +682 Gamboge Herb +683 Alizarin Herb +690 Red Dye +691 Green Dye +692 Dark Blue Dye +693 Yellow Dye +694 Light Blue Dye +695 Pink Dye +696 Black Dye +697 Orange Dye +698 Purple Dye +699 Dark Green Dye +700 Pearl +701 Pile of Ash +703 Sulphur Powder +704 Iron Powder +706 Golden Scorpion Stinger +707 Monster Oil Potion +708 Leather Patch +709 Black Scorpion Stinger +710 Snake Tongue +711 Mountain Snake Tongue +712 Grass Snake Tongue +713 Cave Snake Tongue +718 Silk Cocoon +727 Iten +728 Mouboo Figurine +729 Warped Log +730 Lifestone +732 Druid Tree Branch +740 Root +753 Bat Wing +754 Bat Teeth +763 Terranite Ore +772 Wisp Powder +773 Spectre Powder +774 Poltergeist Powder +775 Bone +776 Skull +777 Rotten Rags +778 Diseased Heart +779 Undead Ear +780 Undead Eye +799 Mylarin Dust +802 Easter Basket +803 Grass Liner +804 Jelly Beans +805 Chocolate Mouboo +806 Reed Bundle +807 Grass Seeds +819 Diamond Powder +820 Ruby Powder +821 Emerald Powder +822 Sapphire Powder +823 Topaz Powder +824 Amethyst Powder +833 Broken Four-Leaf Amulet +834 Broken Doll +835 Hyvern Stinger +836 Grub Slime +841 Red Dotted Wrap +842 Yellow Dotted Wrap +843 Blue Dotted Wrap +844 Purple Striped Wrap +845 Red Golden Striped Wrap +846 Green Red Striped Wrap +847 Plush Mouboo +849 Open Present Box +850 Closed Christmas Box +851 Stick Reinboo +852 Leather Ball +853 Doll +858 Wolvern Tooth +859 Wolvern Pelt +860 Squirrel Pelt +861 White Bell Tuber +862 Iced Water +863 Silver Mirror +864 Book Page +866 Leather Suitcase +869 Antlers +871 Sealed Soul +872 Lock Picks +873 Lazurite Shard +874 Lazurite Crystal +875 Heart of Lazurite +891 Silk Sheet +892 Orange Summon Flower +893 Purple Summon Flower +894 White Summon Flower +895 Yellow Summon Flower +896 Red Summon Flower +1191 White Wrap +1192 Yellow Wrap +1193 Blue Wrap +1194 Purple Wrap +1195 Green Wrap +1198 Soul +1207 Red Christmas Stocking +1208 Red Easter Egg +1209 Green Easter Egg +1210 Blue Easter Egg +1211 Yellow Easter Egg +1212 Pink Easter Egg +1213 Teal Easter Egg +1222 Small Piece of Clay +1223 Big Piece of White Clay +1224 Scroll of Paper +1225 Fire Scroll +1226 Silver Bell +1228 Light Crystal +1233 Cheap Plastic Fangs +1234 Red Christmas Ornament +1235 Yellow Christmas Ornament +1236 Green Christmas Ornament +1237 Aqua Christmas Ornament +1238 Blue Christmas Ornament +1239 Purple Christmas Ornament +1240 Santa Snow Globe +1241 Snowman Snow Globe +1245 Bent Needle +1246 Dark Easter Egg +1249 Strange Coin +1254 Dark Petal +1257 Flawed Lens +2250 Red Cotton Cloth +2251 Green Cotton Cloth +2252 Dark Blue Cotton Cloth +2253 Yellow Cotton Cloth +2254 Light Blue Cotton Cloth +2255 Pink Cotton Cloth +2256 Black Cotton Cloth +2257 Orange Cotton Cloth +2258 Purple Cotton Cloth +2259 Dark Green Cotton Cloth +3000 Jack O Lantern +3002 Realistic Brain +3003 Jar of Blood +3004 Tongue +3011 Pumpkin Seeds +4000 Angry Scorpion Stinger +4001 Coal +4002 Diamond +4003 Ruby +4004 Emerald +4005 Sapphire +4006 Topaz +4007 Amethyst +4015 Iron Ingot +4016 Bandit Hood +4017 Red Powder +4018 Yellow Powder +4019 Blue Powder +4021 Yellow Present Box +4022 White Present Box +4023 Animal Bones +4024 Frozen Yeti Tear +4025 Yeti Claw +4026 Ice Cube +4029 Grimace of Dementia +4034 Black Pearl +4037 White Blanket +4038 White Saddle Rug +4039 Red Saddle Rug +4040 Raw Talisman +4041 Flight Talisman +5110 Blood Stone +5111 Brain Stem +5112 Crypt Key +5113 Ectoplasm +5114 Urn +5115 Vampire Bat Wing +5116 Wraith Horn +5117 Astral Cube +5118 Runestone A +5119 Runestone L +5120 Runestone N +5121 Runestone T +5122 Runestone W +5123 Quill of Binding +5124 Blood Ink +5125 Underworld Key +5126 Empty Jar +5127 Jar of Own Blood +511 Santa Hat +524 Fancy Hat +525 Miner's Hat +543 Standard Headband +544 Silk Headband +615 Pumpkin Helmet +616 Axe Hat +617 Pirate Hat +618 Goggles +619 Leather Goggles +620 Circlet +621 Eyepatch +622 Bandana +627 Top Hat +628 Funky Hat +629 Mush Hat +630 Shroom Hat +633 Christmas Elf Hat +634 Face Mask +636 Warlord Helmet +637 Knight's Helmet +638 Infantry Helmet +639 Crusade Helmet +643 White Cowboy Hat +644 Black Cowboy Hat +646 Crown +647 Developer's Cap +654 Cap +656 Serf Hat +675 Graduation Cap +678 Noh Mask +679 Demon Mask +721 High Priest Crown +722 Monster Skull Helmet +723 Desert Hat +724 Cotton Headband +725 GM Cap +735 Cotton Boots +751 Pinkie Hat +752 Fluffy Hat +759 Paladin's Helmet +760 Overlord's Helmet +761 Desert Helmet +764 Sailor Hat +765 Captain's Hat +766 Terranite Helmet +769 Guy Fawkes Mask +770 Fairy Hat +781 Witch Doctor's Mask +795 Bromenal Helmet +800 Bowler Hat (Brown) +801 Pinkie Helmet +848 Earmuffs +854 Elf Nightcap +855 Sunglasses +856 Knit Cap +877 Bull Helmet +882 Red Eggshell Hat +883 Blue Eggshell Hat +884 Yellow Eggshell Hat +885 Green Eggshell Hat +886 Orange Eggshell Hat +887 Dark Eggshell Hat +888 Magic GM Top Hat +889 Murderer Crown +890 Beanie Copter +897 Red Rose Hat +898 White Rose Hat +899 Pink Rose Hat +900 Yellow Rose Hat +901 Orange Rose Hat +902 Blue Rose Hat +905 Bucket +1173 Tam O' Shanter +1174 Cashiers' Shade +1175 Autumn Mask +1190 Nutcracker Hat +1196 Beret +1203 Ranger Hat +1204 Antler Hat +1205 Christmas Tree Hat +1206 Santa Beard Hat +1214 Bunny Ears +1216 Mouboo Head +1217 Cat Ears +1218 Paper Bag +1219 Moubootaur Head +1220 Bunch of Parsley +1221 Skull Mask +1242 Snow Goggles +1247 Heart Glasses +1255 White Rabbit Ears +1256 Eggshell Hat +1276 Opera Mask +1277 Jester Mask +1278 Witch Hat +1279 Goblin Mask +2130 Red Desert Hat +2131 Green Desert Hat +2132 Dark Blue Desert Hat +2133 Yellow Desert Hat +2134 Light Blue Desert Hat +2135 Pink Desert Hat +2136 Black Desert Hat +2137 Orange Desert Hat +2138 Purple Desert Hat +2139 Dark Green Desert Hat +2140 Red Cotton Headband +2141 Green Cotton Headband +2142 Dark Blue Cotton Headband +2143 Yellow Cotton Headband +2144 Light Blue Cotton Headband +2145 Pink Cotton Headband +2146 Black Cotton Headband +2147 Orange Cotton Headband +2148 Purple Cotton Headband +2149 Dark Green Cotton Headband +2190 Red Rabbit Ears +2191 Green Rabbit Ears +2192 Dark Blue Rabbit Ears +2193 Yellow Rabbit Ears +2194 Light Blue Rabbit Ears +2195 Pink Rabbit Ears +2196 Black Rabbit Ears +2197 Orange Rabbit Ears +2198 Purple Rabbit Ears +2199 Dark Green Rabbit Ears +2200 Red Wizard Hat +2201 Green Wizard Hat +2202 Dark Blue Wizard Hat +2203 Yellow Wizard Hat +2204 Light Blue Wizard Hat +2205 Pink Wizard Hat +2206 Black Wizard Hat +2207 Orange Wizard Hat +2208 Purple Wizard Hat +2209 Dark Green Wizard Hat +2210 Red Bowler Hat +2211 Green Bowler Hat +2212 Dark Blue Bowler Hat +2213 Yellow Bowler Hat +2214 Light Blue Bowler Hat +2215 Pink Bowler Hat +2216 Black Bowler Hat +2217 Orange Bowler Hat +2218 Purple Bowler Hat +2219 Dark Green Bowler Hat +2230 Red Bowler Hat (Brown) +2231 Green Bowler Hat (Brown) +2232 Dark Blue Bowler Hat (Brown) +2233 Yellow Bowler Hat (Brown) +2234 Light Blue Bowler Hat (Brown) +2235 Pink Bowler Hat (Brown) +2236 Black Bowler Hat (Brown) +2237 Orange Bowler Hat (Brown) +2238 Purple Bowler Hat (Brown) +2239 Dark Green Bowler Hat (Brown) +2260 Red Beret +2261 Green Beret +2262 Dark Blue Beret +2263 Yellow Beret +2264 Light Blue Beret +2265 Pink Beret +2266 Black Beret +2267 Orange Beret +2268 Purple Beret +2269 Dark Green Beret +4020 Candle Helmet +4027 Yeti Mask +4028 Wizard Hat +4030 Bowler Hat +4031 Monocle +4032 Pan Hat +4033 Chef Hat +4042 Rednose +5128 Dark Helm +5129 Underworld Mask +5130 Phylactery +521 Dagger +522 Sharp Knife +536 Short Sword +549 Axe +570 Bone Knife +571 Setzer +579 Rock Knife +587 Sword +591 Long Sword +599 Fire Sword +867 Ice Gladius +1171 Wand +1201 Knife +1215 Toy Sabre +523 Leather Shirt +546 Desert Shirt +564 Turtleneck Sweater +624 V-Neck Sweater +625 Chainmail Shirt +626 Light Platemail +645 Golden Platemail +649 White Evoker's Robe (Blue) +650 Black Evoker's Robe (Blue) +651 White Wizard Robe +652 Black Wizard Robe +653 Apprentice Robe +658 Warlord Plate +659 Golden Warlord Plate +688 Tank Top +689 Short Tank Top +720 Silk Robe +726 GM Robe +731 Assassin Pants +755 Assassin Shirt +767 Terranite Chest Armor +782 Forest Armor +783 Platyna Red Dress +791 Yeti Skin Shirt +793 Bromenal Chest +798 Sorcerer Robe (Red) +870 Fine Dress +880 Lazurite Robe +1178 Contributor Shirt +1183 Amber Christmas Sweater +1184 Funky Christmas Sweater +1185 Pink Christmas Sweater +1186 Dark Christmas Sweater +1187 Blue Christmas Sweater +1202 Cotton Shirt +1244 Dark Talisman +2050 Red Cotton Shirt +2051 Green Cotton Shirt +2052 Dark Blue Cotton Shirt +2053 Yellow Cotton Shirt +2054 Light Blue Cotton Shirt +2055 Pink Cotton Shirt +2056 Black Cotton Shirt +2057 Orange Cotton Shirt +2058 Purple Cotton Shirt +2059 Dark Green Cotton Shirt +2060 Red V-Neck Sweater +2061 Green V-Neck Sweater +2062 Dark Blue V-Neck Sweater +2063 Yellow V-Neck Sweater +2064 Light Blue V-Neck Sweater +2065 Pink V-Neck Sweater +2066 Black V-Neck Sweater +2067 Orange V-Neck Sweater +2068 Purple V-Neck Sweater +2069 Dark Green V-Neck Sweater +2070 Red Turtleneck Sweater +2071 Green Turtleneck Sweater +2072 Dark Blue Turtleneck Sweater +2073 Yellow Turtleneck Sweater +2074 Light Blue Turtleneck Sweater +2075 Pink Turtleneck Sweater +2076 Black Turtleneck Sweater +2077 Orange Turtleneck Sweater +2078 Purple Turtleneck Sweater +2079 Dark Green Turtleneck Sweater +2080 Red Silk Robe +2081 Green Silk Robe +2082 Dark Blue Silk Robe +2083 Yellow Silk Robe +2084 Light Blue Silk Robe +2085 Pink Silk Robe +2086 Black Silk Robe +2087 Orange Silk Robe +2088 Purple Silk Robe +2089 Dark Green Silk Robe +2090 Red Tank Top +2091 Green Tank Top +2092 Dark Blue Tank Top +2093 Yellow Tank Top +2094 Light Blue Tank Top +2095 Pink Tank Top +2096 Black Tank Top +2097 Orange Tank Top +2098 Purple Tank Top +2099 Dark Green Tank Top +2120 Red Short Tank Top +2121 Green Short Tank Top +2122 Dark Blue Short Tank Top +2123 Yellow Short Tank Top +2124 Light Blue Short Tank Top +2125 Pink Short Tank Top +2126 Black Short Tank Top +2127 Orange Short Tank Top +2128 Purple Short Tank Top +2129 Dark Green Short Tank Top +2220 Red Sorcerer Robe (Red) +2221 Green Sorcerer Robe (Red) +2222 Dark Blue Sorcerer Robe (Red) +2223 Yellow Sorcerer Robe (Red) +2224 Light Blue Sorcerer Robe (Red) +2225 Pink Sorcerer Robe (Red) +2226 Black Sorcerer Robe (Red) +2227 Orange Sorcerer Robe (Red) +2228 Purple Sorcerer Robe (Red) +2229 Dark Green Sorcerer Robe (Red) +2240 Fine Red Dress +2241 Fine Green Dress +2242 Fine Dark Blue Dress +2243 Fine Yellow Dress +2244 Fine Light Blue Dress +2245 Fine Pink Dress +2246 Fine Black Dress +2247 Fine Orange Dress +2248 Fine Purple Dress +2249 Fine Dark Green Dress +5000 Red Sorcerer Robe (Green) +5001 Green Sorcerer Robe (Green) +5002 Dark Blue Sorcerer Robe (Green) +5003 Yellow Sorcerer Robe (Green) +5004 Light Blue Sorcerer Robe (Green) +5005 Pink Sorcerer Robe (Green) +5006 Black Sorcerer Robe (Green) +5007 Orange Sorcerer Robe (Green) +5008 Purple Sorcerer Robe (Green) +5009 Dark Green Sorcerer Robe (Green) +5010 Sorcerer Robe (Green) +5011 Red Sorcerer Robe (Dark Blue) +5012 Green Sorcerer Robe (Dark Blue) +5013 Dark Blue Sorcerer Robe (Dark Blue) +5014 Yellow Sorcerer Robe (Dark Blue) +5015 Light Blue Sorcerer Robe (Dark Blue) +5016 Pink Sorcerer Robe (Dark Blue) +5017 Black Sorcerer Robe (Dark Blue) +5018 Orange Sorcerer Robe (Dark Blue) +5019 Purple Sorcerer Robe (Dark Blue) +5020 Dark Green Sorcerer Robe (Dark Blue) +5021 Sorcerer Robe (Dark Blue) +5022 Red Sorcerer Robe (Yellow) +5023 Green Sorcerer Robe (Yellow) +5024 Dark Blue Sorcerer Robe (Yellow) +5025 Yellow Sorcerer Robe (Yellow) +5026 Light Blue Sorcerer Robe (Yellow) +5027 Pink Sorcerer Robe (Yellow) +5028 Black Sorcerer Robe (Yellow) +5029 Orange Sorcerer Robe (Yellow) +5030 Purple Sorcerer Robe (Yellow) +5031 Dark Green Sorcerer Robe (Yellow) +5032 Sorcerer Robe (Yellow) +5033 Red Sorcerer Robe (Light Blue) +5034 Green Sorcerer Robe (Light Blue) +5035 Dark Blue Sorcerer Robe (Light Blue) +5036 Yellow Sorcerer Robe (Light Blue) +5037 Light Blue Sorcerer Robe (Light Blue) +5038 Pink Sorcerer Robe (Light Blue) +5039 Black Sorcerer Robe (Light Blue) +5040 Orange Sorcerer Robe (Light Blue) +5041 Purple Sorcerer Robe (Light Blue) +5042 Dark Green Sorcerer Robe (Light Blue) +5043 Sorcerer Robe (Light Blue) +5044 Red Sorcerer Robe (Pink) +5045 Green Sorcerer Robe (Pink) +5046 Dark Blue Sorcerer Robe (Pink) +5047 Yellow Sorcerer Robe (Pink) +5048 Light Blue Sorcerer Robe (Pink) +5049 Pink Sorcerer Robe (Pink) +5050 Black Sorcerer Robe (Pink) +5051 Orange Sorcerer Robe (Pink) +5052 Purple Sorcerer Robe (Pink) +5053 Dark Green Sorcerer Robe (Pink) +5054 Sorcerer Robe (Pink) +5055 Red Sorcerer Robe (Black) +5056 Green Sorcerer Robe (Black) +5057 Dark Blue Sorcerer Robe (Black) +5058 Yellow Sorcerer Robe (Black) +5059 Light Blue Sorcerer Robe (Black) +5060 Pink Sorcerer Robe (Black) +5061 Black Sorcerer Robe (Black) +5062 Orange Sorcerer Robe (Black) +5063 Purple Sorcerer Robe (Black) +5064 Dark Green Sorcerer Robe (Black) +5065 Sorcerer Robe (Black) +5066 Red Sorcerer Robe (Orange) +5067 Green Sorcerer Robe (Orange) +5068 Dark Blue Sorcerer Robe (Orange) +5069 Yellow Sorcerer Robe (Orange) +5070 Light Blue Sorcerer Robe (Orange) +5071 Pink Sorcerer Robe (Orange) +5072 Black Sorcerer Robe (Orange) +5073 Orange Sorcerer Robe (Orange) +5074 Purple Sorcerer Robe (Orange) +5075 Dark Green Sorcerer Robe (Orange) +5076 Sorcerer Robe (Orange) +5077 Red Sorcerer Robe (Purple) +5078 Green Sorcerer Robe (Purple) +5079 Dark Blue Sorcerer Robe (Purple) +5080 Yellow Sorcerer Robe (Purple) +5081 Light Blue Sorcerer Robe (Purple) +5082 Pink Sorcerer Robe (Purple) +5083 Black Sorcerer Robe (Purple) +5084 Orange Sorcerer Robe (Purple) +5085 Purple Sorcerer Robe (Purple) +5086 Dark Green Sorcerer Robe (Purple) +5087 Sorcerer Robe (Purple) +5088 Red Sorcerer Robe (Dark Green) +5089 Green Sorcerer Robe (Dark Green) +5090 Dark Blue Sorcerer Robe (Dark Green) +5091 Yellow Sorcerer Robe (Dark Green) +5092 Light Blue Sorcerer Robe (Dark Green) +5093 Pink Sorcerer Robe (Dark Green) +5094 Black Sorcerer Robe (Dark Green) +5095 Orange Sorcerer Robe (Dark Green) +5096 Purple Sorcerer Robe (Dark Green) +5097 Dark Green Sorcerer Robe (Dark Green) +5098 Sorcerer Robe (Dark Green) +5099 Red Sorcerer Robe (White) +5100 Green Sorcerer Robe (White) +5101 Dark Blue Sorcerer Robe (White) +5102 Yellow Sorcerer Robe (White) +5103 Light Blue Sorcerer Robe (White) +5104 Pink Sorcerer Robe (White) +5105 Black Sorcerer Robe (White) +5106 Orange Sorcerer Robe (White) +5107 Purple Sorcerer Robe (White) +5108 Dark Green Sorcerer Robe (White) +5109 Sorcerer Robe (White) +5131 Red Contributor Shirt +5132 Green Contributor Shirt +5133 Dark Blue Contributor Shirt +5134 Yellow Contributor Shirt +5135 Light Blue Contributor Shirt +5136 Pink Contributor Shirt +5137 Black Contributor Shirt +5138 Orange Contributor Shirt +5139 Purple Contributor Shirt +5140 Dark Green Contributor Shirt +528 Boots +655 Fur Boots +734 Black Boots +757 Assassin Boots +792 Bromenal Boots +876 Warlord Boots +1188 Red Stockings +2150 Red Cotton Boots +2151 Green Cotton Boots +2152 Dark Blue Cotton Boots +2153 Yellow Cotton Boots +2154 Light Blue Cotton Boots +2155 Pink Cotton Boots +2156 Black Cotton Boots +2157 Orange Cotton Boots +2158 Purple Cotton Boots +2159 Dark Green Cotton Boots +529 Iron Arrow +762 Terranite Arrow +904 Sling Bullet +1199 Arrow +1282 Bone Arrows +530 Short Bow +545 Forest Bow +548 Halberd +588 Bastard Sword +594 Spear +595 Heavy Spear +596 Pike +597 Heavy Pike +623 Scythe +758 Wooden Staff +878 Banshee Bow +903 Sling Shot +906 Kid Book +1200 Bow +531 Miner Gloves +532 Leather Gloves +563 Winter Gloves +585 Scarab Armlet +741 Cotton Gloves +756 Assassin Gloves +794 Bromenal Gloves +868 Silk Gloves +2160 Red Cotton Gloves +2161 Green Cotton Gloves +2162 Dark Blue Cotton Gloves +2163 Yellow Cotton Gloves +2164 Light Blue Cotton Gloves +2165 Pink Cotton Gloves +2166 Black Cotton Gloves +2167 Orange Cotton Gloves +2168 Purple Cotton Gloves +2169 Dark Green Cotton Gloves +586 Cotton Shorts +610 Jeans Shorts +632 Cotton Skirt +642 Jeans Chaps +648 Cotton Trousers +768 Terranite Legs +771 Miniskirt +796 Bromenal Legs +857 Leather Trousers +881 Ragged Shorts +1172 Silk Pants +2100 Red Cotton Skirt +2101 Green Cotton Skirt +2102 Dark Blue Cotton Skirt +2103 Yellow Cotton Skirt +2104 Light Blue Cotton Skirt +2105 Pink Cotton Skirt +2106 Black Cotton Skirt +2107 Orange Cotton Skirt +2108 Purple Cotton Skirt +2109 Dark Green Cotton Skirt +2110 Red Cotton Shorts +2111 Green Cotton Shorts +2112 Dark Blue Cotton Shorts +2113 Yellow Cotton Shorts +2114 Light Blue Cotton Shorts +2115 Pink Cotton Shorts +2116 Black Cotton Shorts +2117 Orange Cotton Shorts +2118 Purple Cotton Shorts +2119 Dark Green Cotton Shorts +2170 Red Miniskirt +2171 Green Miniskirt +2172 Dark Blue Miniskirt +2173 Yellow Miniskirt +2174 Light Blue Miniskirt +2175 Pink Miniskirt +2176 Black Miniskirt +2177 Orange Miniskirt +2178 Purple Miniskirt +2179 Dark Green Miniskirt +2180 Red Cotton Trousers +2181 Green Cotton Trousers +2182 Dark Blue Cotton Trousers +2183 Yellow Cotton Trousers +2184 Light Blue Cotton Trousers +2185 Pink Cotton Trousers +2186 Black Cotton Trousers +2187 Orange Cotton Trousers +2188 Purple Cotton Trousers +2189 Dark Green Cotton Trousers +601 Steel Shield +602 Wooden Shield +603 Leather Shield +677 Heart Necklace +742 Four-Leaf Clover +749 Towel +829 Crozenite Four-Leaf Amulet +830 Bromenal Four-Leaf Amulet +831 Silver Four-Leaf Amulet +832 Golden Four-Leaf Amulet +865 Grimoire +879 Heart of Isis +1197 Jack's Skeleton Charm +1227 Enchanter's Amulet +702 Wedding Ring +4008 Diamond Ring +4009 Ruby Ring +4010 Emerald Ring +4011 Sapphire Ring +4012 Topaz Ring +4013 Amethyst Ring +4014 Simple Ring diff --git a/loggers.py b/loggers.py new file mode 100644 index 0000000..60e11ff --- /dev/null +++ b/loggers.py @@ -0,0 +1,5 @@ +import logging + +netlog = logging.getLogger("ManaChat.Net") +debuglog = logging.getLogger("ManaChat.Debug") +chatlog = logging.getLogger("ManaChat.Chat") diff --git a/loggers.pyc b/loggers.pyc Binary files differnew file mode 100644 index 0000000..8fc5712 --- /dev/null +++ b/loggers.pyc diff --git a/logicmanager.py b/logicmanager.py new file mode 100644 index 0000000..1745948 --- /dev/null +++ b/logicmanager.py @@ -0,0 +1,21 @@ +import time + + +class LogicManager: + def __init__(self, min_dt=0.1): + self._logic_handlers = [] + self._last_ts = 0 + self._min_dt = min_dt + + def add_logic(self, func): + self._logic_handlers.append(func) + + def tick(self, **kwargs): + now = time.time() + if now - self._last_ts >= self._min_dt: + self._last_ts = now + for f in self._logic_handlers: + f(now, **kwargs) + + +logic_manager = LogicManager() diff --git a/logicmanager.pyc b/logicmanager.pyc Binary files differnew file mode 100644 index 0000000..98be8f0 --- /dev/null +++ b/logicmanager.pyc @@ -0,0 +1,23 @@ +#!/usr/bin/python2 + +try: + import construct + import plyer + # import pytmx + del construct + del plyer + # del pytmx +except ImportError: + import os + import sys + sys.path.append(os.path.join(os.getcwd(), "external")) + +from gui.managui import ManaGuiApp + + +def main(): + ManaGuiApp().run() + + +if __name__ == "__main__": + main() diff --git a/manachat.ini b/manachat.ini new file mode 100644 index 0000000..1983233 --- /dev/null +++ b/manachat.ini @@ -0,0 +1,35 @@ +[Server] +host = 52.174.196.146 +port = 6901 + +[Player] +username = * +password = * +charname = Liviobot + +[Other] +online_txt_url = http://server.themanaworld.org/online.txt +log_network_packets = 0 + +[Plugins] +autospell = 0 +msgqueue = 0 +chatlogfile = 0 +shop = 1 +autofollow = 0 +manaboy = 1 + +[msgqueue] +net.mapserv.cmsg_chat_whisper = 7.5 + +[notify] +notif_timeout = 7000 +notif_sound = 1 + +[chatlogfile] +chatlog_dir = chatlogs + +[shop] +timeout = 60 +shoplist_txt = shoplist.txt +admins_file = shopAdmins.txt diff --git a/manachat.json b/manachat.json new file mode 100644 index 0000000..5766278 --- /dev/null +++ b/manachat.json @@ -0,0 +1,124 @@ +[ + { "type" : "title", + "title" : "Server" }, + + { "type" : "string", + "title" : "Host", + "desc" : "IP or hostname of the TMWA-compatible server", + "section" : "Server", + "key" : "host" }, + + { "type" : "numeric", + "title" : "Port", + "desc" : "Numeric port of the TMWA-compatible login server", + "section" : "Server", + "key" : "port" }, + + { "type" : "title", + "title" : "Player" }, + + { "type" : "string", + "title" : "Username", + "desc" : "Account name", + "section" : "Player", + "key" : "username" }, + + { "type" : "password", + "title" : "Password", + "desc" : "Account password", + "section" : "Player", + "key" : "password" }, + + { "type" : "string", + "title" : "Character name", + "desc" : "Character to use. The character must exist.", + "section" : "Player", + "key" : "charname" }, + + { "type" : "title", + "title" : "Other" }, + + { "type" : "string", + "title" : "Online players list source", + "desc" : "The source of online.txt file, containing current online users.", + "section" : "Other", + "key" : "online_txt_url" }, + + { "type" : "bool", + "title" : "Log network packets", + "desc" : "Used for debugging. Log file will be saved in $TEMP/netlog.txt", + "section" : "Other", + "key" : "log_network_packets" }, + + { "type" : "title", + "title" : "Plugins" }, + + { "type" : "bool", + "title" : "notify", + "desc" : "Desktop notifications.", + "section" : "Plugins", + "key" : "notify" }, + + { "type" : "bool", + "title" : "chatlogfile", + "desc" : "Log chat history to files.", + "section" : "Plugins", + "key" : "chatlogfile" }, + + { "type" : "bool", + "title" : "shop", + "desc" : "Enable shop mode.", + "section" : "Plugins", + "key" : "shop" }, + + { "type" : "bool", + "title" : "autofollow", + "desc" : "Add autofollow (/follow Player).", + "section" : "Plugins", + "key" : "autofollow" }, + + { "type" : "title", + "title" : "Notifications" }, + + { "type" : "numeric", + "title" : "Notifications timeout", + "desc" : "Seconds (or milliseconds) before notifications disappear.", + "section" : "notify", + "key" : "notif_timeout" }, + + { "type" : "bool", + "title" : "Notification sound", + "desc" : "Play notification sound.", + "section" : "notify", + "key" : "notif_sound" }, + + { "type" : "title", + "title" : "Chat logs" }, + + { "type" : "path", + "title" : "Chat log directory", + "desc" : "Created automatically.", + "section" : "chatlogfile", + "key" : "chatlog_dir" }, + + { "type" : "title", + "title" : "Shop mode" }, + + { "type" : "numeric", + "title" : "Max trade time", + "desc" : "Max time (seconds) before shop auto-cancels trade.", + "section" : "shop", + "key" : "timeout" }, + + { "type" : "path", + "title" : "shoplist.txt file", + "desc" : "Path to ManaPlus-compatible shoplist.txt", + "section" : "shop", + "key" : "shoplist_txt" }, + + { "type" : "path", + "title" : "File with shop admins", + "desc" : "Each line contains a name of a shop admin. Can be empty", + "section" : "shop", + "key" : "admins_file" } +] diff --git a/mapdb.zip b/mapdb.zip Binary files differnew file mode 100644 index 0000000..0efdc80 --- /dev/null +++ b/mapdb.zip diff --git a/mapnames.py b/mapnames.py new file mode 100644 index 0000000..5c01280 --- /dev/null +++ b/mapnames.py @@ -0,0 +1,19 @@ + +map_names = {} + + +def read_map_names(file='mapnames.txt'): + with open(file) as f: + for l in f.readlines(): + try: + index = l.index(' ') + map_id = l[:index] + map_name = l[index + 1:-1] + map_names[map_id] = map_name + except ValueError: + pass + + return map_names + +if len(map_names) < 1: + read_map_names() diff --git a/mapnames.pyc b/mapnames.pyc Binary files differnew file mode 100644 index 0000000..4013eb2 --- /dev/null +++ b/mapnames.pyc diff --git a/mapnames.txt b/mapnames.txt new file mode 100644 index 0000000..03bc6a4 --- /dev/null +++ b/mapnames.txt @@ -0,0 +1,104 @@ +001-1 Tulimshar Port +001-2 South Tulimshar Indoor +001-3 Tulimshar Arena +002-1 Tulimshar South +002-2 Sandstorm Desert Indoors +002-3 Tulimshar Mining Camp +002-4 Desert Mines +002-5 Deep Desert Mines +004-3 Pirate Caves First Floor +004-4 Pirate Caves Second Floor +004-5 Pirate Den +005-3 Snake Pit +006-1 Desert Mountains +006-2 Pachua's Village +006-3 Desert Mountain Cave +007-1 Woodland +007-2 Illia forsaken inn +008-1 Hurnscald Outskirts +009-1 Hurnscald +009-2 Hurnscald Indoor +009-3 Hurnscald Cave +009-4 Orum Caves +009-5 Last man standing +009-6 Cave +009-7 The Sanguine Vault +010-1 Woodland +010-2 Dimond's Cove +011-1 Woodland +011-3 Hermit's Cave +011-4 Lake Cave +011-6 Bandit Cave +012-1 Woodland Hills +012-3 Moggun Cave +012-4 Terranite Cave +013-1 Woodland Hills +013-2 Magic House +013-3 Woodland Hills Cave +014-1 Woodland +014-3 Woodland Cave +015-1 Woodland +015-3 Cat's Cave +016-1 Woodland +017-1 Woodland Hills +017-2 Theater +017-3 Woodland Cave +017-4 Hideout +017-9 GM Lounge +018-1 Woodland Mining Camp +018-2 Woodland Mining Camp Indoor +018-3 Northern Mines +019-1 snow Field +019-3 Snow Cave +019-4 Snow Cave +020-1 Nivalis +020-2 Nivalis Indoor +020-3 Ice cave +021-3 Central Tulimshar Sewers +025-1 Woodland Swamp +025-3 Rossy Main Hall +025-4 Rossy Battle Caves +026-1 Swamp +027-1 Graveyard +027-2 Graveyard Indoor +027-3 Crypt Basement +027-4 Crypt Sub-Basement One +027-5 Crypt Sub-Basement Two +027-6 Crypt Sub-Basement Two +027-7 Crypt Sub-Basement Two +027-8 Crypt Sub-Basement Two +028-1 GM Island +028-3 GM Island Cave +029-1 Candor Island +029-2 Candor Island Indoor +029-3 Candor Cave +030-2 Christmas Inn +030-3 The Frozen Lake +030-4 Christmas Inn Warehouse +031-1 Nivalis Port +031-2 Angela's House +031-3 Ice Labyrinth +031-4 Cindy Cave +032-3 Outback Cave +033-1 snow Path +034-1 Snow Forest +034-2 Trappers Huts +035-2 Koga - Main +036-2 Koga - Candor +043-3 Sandy Dungeon Level 1 +043-4 Sandy Dungeon Level 2 +045-1 Deep Snow Forest +046-1 Rock Plateau +046-3 Frosty Underground +047-1 snow Hills +047-3 Snow Hills Cave +048-2 Blue Sages' Mansion +051-1 Illia outskirts +051-3 Illia Bandit Cave +052-1 Illia archipelago +052-2 Illia forsaken inn +055-1 Woodland Hills +055-3 Cave +056-2 Mirak's House +057-1 Woodland +botcheck Botcheck Area diff --git a/monsterdb.py b/monsterdb.py new file mode 100644 index 0000000..5235b5f --- /dev/null +++ b/monsterdb.py @@ -0,0 +1,16 @@ + +monster_db = {} + + +def read_monster_db(file='monsterdb.txt'): + with open(file) as f: + for l in f.readlines(): + try: + index = l.index(' ') + monster_id = int(l[:index]) + monster_name = l[index + 1:-1] + monster_db[monster_id] = monster_name + except ValueError: + pass + + return monster_db diff --git a/monsterdb.pyc b/monsterdb.pyc Binary files differnew file mode 100644 index 0000000..0186ebd --- /dev/null +++ b/monsterdb.pyc diff --git a/monsterdb.txt b/monsterdb.txt new file mode 100644 index 0000000..74c5bbe --- /dev/null +++ b/monsterdb.txt @@ -0,0 +1,127 @@ +1002 Maggot +1003 Scorpion +1004 Red Scorpion +1005 Green Slime +1006 Giant Maggot +1007 Yellow Slime +1008 Red Slime +1009 Black Scorpion +1010 Snake +1011 Fire Goblin +1012 Spider +1013 Evil Mushroom +1014 Pink Flower +1015 Santa Slime +1016 Rudolph Slime +1017 Bat +1018 Pinkie +1019 Spiky Mushroom +1020 Fluffy +1021 Cave Snake +1022 Jack O +1023 Fire Skull +1024 Poison Skull +1025 Log Head +1026 Mountain Snake +1027 Easter Fluffy +1028 Mouboo +1029 Mauve Plant +1030 Cobalt Plant +1031 Gamboge Plant +1032 Alizarin Plant +1033 Sea Slime +1034 Grass Snake +1035 Silkworm +1036 Zombie +1037 Clover Patch +1038 Squirrel +1039 Fire Lizard +1040 Wisp +1041 Snail +1042 Spectre +1043 Skeleton +1044 Lady Skeleton +1045 Fallen +1046 Tame Scorpion +1047 Poltergeist +1048 Duck +1049 Bee +1050 House Maggot +1054 Troll +1055 Butterfly +1056 Cave Maggot +1057 Angry Scorpion +1058 Ice Goblin +1059 Giant Cave Maggot +1060 Archant +1061 Moggun +1062 Terranite +1063 Pumpkin +1064 Bandit +1065 Bandit Lord +1066 Vampire Bat +1067 Reaper +1068 Reaper +1069 Scythe +1070 Ball Lightning +1071 Ice Element +1072 Yeti +1073 The Lost +1074 Red Bone +1075 Stalker +1077 Drunken Skeleton +1078 Tipsy Skeleton +1079 Drunken Lady Skeleton +1080 Blue Spark +1081 Red Spark +1082 Serqet +1083 Huntsman Spider +1084 Crotcher Scorpion +1085 Ice Skull +1086 Fey Element +1087 Larvern +1088 Hyvern +1089 Hungry Fluffy +1090 Wolvern +1091 Blue Slime +1092 Slime Blast +1093 White Slime +1094 Reinboo +1095 White Bell +1096 Soul Snake +1097 Soul Eater +1098 Copper Slime +1099 Sleeping Bandit +1100 Azul Slime +1101 Demonic Spirit +1102 Luvia +1103 Witch Guard +1104 Demonic Mouboo +1105 Vicious Squirrel +1106 Wicked Mushroom +1107 Bluepar +1108 Angry Fire Goblin +1109 Angry Sea Slime +1110 Angry Green Slime +1111 Candied Slime +1112 Santaboo +1113 Pollett +1114 Nut Cracker +1115 Sea Slime Mother +1116 Undead Witch +1117 Undead Troll +1118 Green Slime Mother +1119 Thug +1120 Swashbuckler +1121 Grenadier +1122 The Dread Pirate Marley +1123 The Dread Pirate Marley +1124 Wight +1125 Mana Ghost +1126 Psi-Brain +1127 General Krukan +1128 General Razha +1129 General Terogan +1130 Moonshroom +1131 Mana Bug +1132 Lava Slime diff --git a/net/__init__.py b/net/__init__.py new file mode 100644 index 0000000..55c1c56 --- /dev/null +++ b/net/__init__.py @@ -0,0 +1,11 @@ +import loginsrv + + +def login(host, port, username, password, charname): + loginsrv.connect(host, port) + + loginsrv.server.username = username + loginsrv.server.password = password + loginsrv.server.char_name = charname + + loginsrv.cmsg_server_version_request() diff --git a/net/__init__.pyc b/net/__init__.pyc Binary files differnew file mode 100644 index 0000000..6ec9624 --- /dev/null +++ b/net/__init__.pyc diff --git a/net/__pycache__/__init__.cpython-37.pyc b/net/__pycache__/__init__.cpython-37.pyc Binary files differnew file mode 100644 index 0000000..968d236 --- /dev/null +++ b/net/__pycache__/__init__.cpython-37.pyc diff --git a/net/being.py b/net/being.py new file mode 100644 index 0000000..ad7ad57 --- /dev/null +++ b/net/being.py @@ -0,0 +1,63 @@ +import monsterdb + + +def job_type(job): + if (job <= 25 or (job >= 4001 and job <= 4049)): + return "player" + elif (job >= 46 and job <= 1000): + return "npc" + elif (job > 1000 and job <= 2000): + return "monster" + elif (job == 45): + return "portal" + + +class Being: + def __init__(self, being_id, job): + self.id = being_id + self.job = job + self.speed = 0 + self.x = 0 + self.y = 0 + + if job_type(job) == "monster": + self._name = monsterdb.monster_db.get(job, "") + else: + self._name = "" + + @property + def name(self): + if len(self._name) > 0: + return self._name + return "{{ID:" + str(self.id) + "}}" + + @name.setter + def name(self, newname): + self._name = newname + + @property + def type(self): + return job_type(self.job) + + def __repr__(self): + return self.name + + +class BeingsCache(dict): + + def __init__(self, name_request_func, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self._name_request_func = name_request_func + + def findId(self, name, type_="player"): + for id_, being in self.iteritems(): + if being.name == name and being.type == type_: + return id_ + return -10 + + def findName(self, id_, job=1): + if id_ not in self: + self[id_] = Being(id_, job) + if job_type(job) in ("player", "npc"): + self._name_request_func(id_) + return self[id_].name diff --git a/net/being.pyc b/net/being.pyc Binary files differnew file mode 100644 index 0000000..32916bd --- /dev/null +++ b/net/being.pyc diff --git a/net/charserv.py b/net/charserv.py new file mode 100644 index 0000000..50f6f99 --- /dev/null +++ b/net/charserv.py @@ -0,0 +1,142 @@ + +from construct import * +from construct.protocols.layer3.ipv4 import IpAddress +import mapserv +import stats +from protocol import * +from utils import * +from common import * +from loggers import netlog + +server = None + + +def smsg_ignore(data): + pass + + +@extendable +def smsg_char_login(data): + netlog.info("SMSG_CHAR_LOGIN {}".format(data)) + + char_slot = -1 + for c in data.chars: + if c.name == server.char_name: + char_slot = c.slot + mapserv.player_money = c.money + mapserv.player_stats[stats.EXP] = c.exp + mapserv.player_stats[stats.MONEY] = c.money + mapserv.player_stats[stats.JOB] = c.job + mapserv.player_stats[stats.CHAR_POINTS] = c.charpoints + mapserv.player_stats[stats.HP] = c.hp + mapserv.player_stats[stats.MAX_HP] = c.max_hp + mapserv.player_stats[stats.MP] = c.mp + mapserv.player_stats[stats.MAX_MP] = c.max_mp + mapserv.player_stats[stats.WALK_SPEED] = c.speed + mapserv.player_stats[stats.LEVEL] = c.level + mapserv.player_stats[stats.SKILL_POINTS] = c.skillpoints + break + if char_slot < 0: + err_msg = "CharName {} not found".format(server.char_name) + netlog.error(err_msg) + server.close() + raise Exception(err_msg) + else: + cmsg_char_select(char_slot) + + +@extendable +def smsg_char_login_error(data): + err_msg = "SMSG_CHAR_LOGIN_ERROR (code={})".format(data.code) + netlog.error(err_msg) + server.close() + raise Exception(err_msg) + + +@extendable +def smsg_char_map_info(data): + netlog.info("SMSG_CHAR_MAP_INFO CID={} map={} address={} port={}".format( + data.char_id, data.map_name, data.address, data.port)) + server.close() + + mapserv.connect(data.address, data.port) + mapserv.server.char_name = server.char_name + mapserv.server.char_id = data.char_id + mapserv.player_pos['map'] = data.map_name + mapserv.cmsg_map_server_connect(server.account, data.char_id, + server.session1, server.session2, + server.gender) + + +protodef = { + 0x8000 : (smsg_ignore, Field("data", 2)), + 0x006b : (smsg_char_login, + Struct("data", + ULInt16("length"), + ULInt16("slots"), + Byte("version"), + # Probe("debug", show_stream=False, show_stack=False), + Padding(17), + Array(lambda ctx: (ctx["length"] - 24) / 106, + Struct("chars", + ULInt32("id"), + ULInt32("exp"), + ULInt32("money"), + ULInt32("job"), + ULInt32("job_level"), + Padding(20), + ULInt16("charpoints"), + ULInt16("hp"), + ULInt16("max_hp"), + ULInt16("mp"), + ULInt16("max_mp"), + ULInt16("speed"), + Padding(6), + ULInt16("level"), + ULInt16("skillpoints"), + Padding(12), + StringZ("name", 24), + Padding(6), + Byte("slot"), + Padding(1))))), + 0x006c : (smsg_char_login_error, + Struct("data", Byte("code"))), + 0x0071 : (smsg_char_map_info, + Struct("data", + ULInt32("char_id"), + StringZ("map_name", 16), + IpAddress("address"), + ULInt16("port"))), + 0x0081 : (mapserv.smsg_connection_problem, + Struct("data", Byte("code"))), +} + + +def cmsg_char_server_connect(account, session1, session2, proto, gender): + netlog.info(("CMSG_CHAR_SERVER_CONNECT account={} session1={} " + "session2={} proto={} gender={}").format( + account, session1, session2, proto, gender)) + + # save session data + server.account = account + server.session1 = session1 + server.session2 = session2 + server.gender = gender + + send_packet(server, CMSG_CHAR_SERVER_CONNECT, + (ULInt32("account"), account), + (ULInt32("session1"), session1), + (ULInt32("session2"), session2), + (ULInt16("proto"), proto), + (Gender("gender"), gender)) + + +def cmsg_char_select(slot): + netlog.info("CMSG_CHAR_SELECT slot={}".format(slot)) + send_packet(server, CMSG_CHAR_SELECT, (Byte("slot"), slot)) + + +def connect(host, port): + global server + #server = SocketWrapper(host=host, port=port, protodef=protodef) + server = SocketWrapper(host="52.174.196.146", port=port, protodef=protodef) diff --git a/net/charserv.pyc b/net/charserv.pyc Binary files differnew file mode 100644 index 0000000..1109add --- /dev/null +++ b/net/charserv.pyc diff --git a/net/common.py b/net/common.py new file mode 100644 index 0000000..a589b44 --- /dev/null +++ b/net/common.py @@ -0,0 +1,106 @@ +import time +import socket +import asyncore +from logging import DEBUG +from construct import Struct, ULInt16, String, Enum, Byte +from dispatcher import dispatch +from loggers import netlog + + +def StringZ(name, length, **kw): + kw['padchar'] = "\x00" + kw['paddir'] = "right" + return String(name, length, **kw) + + +def Gender(name): + return Enum(Byte(name), BOY=1, GIRL=0, UNSPECIFIED=2, OTHER=3) + + +class SocketWrapper(asyncore.dispatcher_with_send): + """ + socket wrapper with file-like read() and write() methods + """ + def __init__(self, host=None, port=0, + protodef={}, onerror=None, sock=None): + asyncore.dispatcher_with_send.__init__(self, sock) + self.read_buffer = '' + if sock is None: + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setblocking(1) + self.socket.settimeout(0.7) + + self.protodef = protodef + self.raw = False + self.onerror = onerror + if protodef == {}: + netlog.warning("protodef is empty") + if host is not None: + self.connect((host, port)) + + def handle_read(self): + try: + self.read_buffer += self.recv(2) + except socket.error: + return + while len(self.read_buffer) > 0: + dispatch(self, self.protodef) + + def handle_error(self): + if self.onerror is not None: + self.onerror() + else: + raise + + def read(self, n=-1): + data = '' + if n < 0: + data = self.read_buffer + self.read_buffer = '' + else: + tries = 0 + while len(self.read_buffer) < n: + try: + self.read_buffer += self.recv(n - len(self.read_buffer)) + except socket.error as e: + tries += 1 + if tries < 10: + netlog.error("socket.error %s", e) + time.sleep(0.2) + else: + raise + + data = self.read_buffer[:n] + self.read_buffer = self.read_buffer[n:] + + if netlog.isEnabledFor(DEBUG): + netlog.debug("read " + + ":".join("{:02x}".format(ord(c)) for c in data)) + return data + + def write(self, data): + if netlog.isEnabledFor(DEBUG): + netlog.debug("write " + + ":".join("{:02x}".format(ord(c)) for c in data)) + if self.raw: + self.socket.sendall(data) + else: + self.send(data) + + +def send_packet(srv, opcode_, *fields): + class C: + opcode = opcode_ + + ms = [ULInt16("opcode")] + + for macro, value in fields: + setattr(C, macro.name, value) + ms.append(macro) + + d = Struct("packet", *ms) + d.build_stream(C, srv) + + +def distance(x1, y1, x2, y2): + return max(abs(x2 - x1), abs(y2 - y1)) diff --git a/net/common.pyc b/net/common.pyc Binary files differnew file mode 100644 index 0000000..3f457a5 --- /dev/null +++ b/net/common.pyc diff --git a/net/dispatcher.py b/net/dispatcher.py new file mode 100644 index 0000000..115212c --- /dev/null +++ b/net/dispatcher.py @@ -0,0 +1,27 @@ +from construct import Struct, ULInt16, MetaField +from loggers import netlog +from packetlen import packet_lengths + + +def dispatch(stream, protodef): + opcode = ULInt16("opcode").parse_stream(stream) + + if opcode in protodef: + func, macro = protodef[opcode] + data = macro.parse_stream(stream) + func(data) + else: + data = '' + pktlen = packet_lengths.get(opcode, -1) + + if pktlen > 0: + data = stream.read(pktlen - 2) + elif pktlen == -1: + datadef = Struct("data", + ULInt16("length"), + MetaField("ignore", + lambda ctx: ctx["length"] - 4)) + data = datadef.parse_stream(stream) + + netlog.warning('UNIMPLEMENTED opcode={:04x} data={}'.format( + opcode, data)) diff --git a/net/dispatcher.pyc b/net/dispatcher.pyc Binary files differnew file mode 100644 index 0000000..c671b01 --- /dev/null +++ b/net/dispatcher.pyc diff --git a/net/inventory.py b/net/inventory.py new file mode 100644 index 0000000..de927d7 --- /dev/null +++ b/net/inventory.py @@ -0,0 +1,51 @@ +import mapserv + + +def get_item_index(item_id): + for index, (id_, _) in mapserv.player_inventory.iteritems(): + if id_ == item_id: + return index + + return -10 + + +def remove_from_inventory(index, amount): + item_id, curr_amount = mapserv.player_inventory[index] + curr_amount -= amount + if curr_amount <= 0: + del mapserv.player_inventory[index] + else: + mapserv.player_inventory[index] = item_id, curr_amount + + +def add_to_inventory(index, item_id, amount): + if index not in mapserv.player_inventory: + mapserv.player_inventory[index] = item_id, amount + else: + _, curr_amount = mapserv.player_inventory[index] + mapserv.player_inventory[index] = item_id, curr_amount + amount + + +def get_storage_index(item_id): + for index, (id_, _) in mapserv.player_storage.iteritems(): + if id_ == item_id: + return index + + return -10 + + +def remove_from_storage(index, amount): + item_id, curr_amount = mapserv.player_storage[index] + curr_amount -= amount + if curr_amount <= 0: + del mapserv.player_storage[index] + else: + mapserv.player_storage[index] = item_id, curr_amount + + +def add_to_storage(index, item_id, amount): + if index not in mapserv.player_storage: + mapserv.player_storage[index] = item_id, amount + else: + _, curr_amount = mapserv.player_storage[index] + mapserv.player_storage[index] = item_id, curr_amount + amount diff --git a/net/inventory.pyc b/net/inventory.pyc Binary files differnew file mode 100644 index 0000000..85b5a74 --- /dev/null +++ b/net/inventory.pyc diff --git a/net/item.py b/net/item.py new file mode 100644 index 0000000..f82fa77 --- /dev/null +++ b/net/item.py @@ -0,0 +1,8 @@ + +class FloorItem: + def __init__(self, obj_id, item_type, amount, x, y): + self.id = obj_id + self.type = item_type + self.amount = amount + self.x = x + self.y = y diff --git a/net/item.pyc b/net/item.pyc Binary files differnew file mode 100644 index 0000000..9ceb6a7 --- /dev/null +++ b/net/item.pyc diff --git a/net/loginsrv.py b/net/loginsrv.py new file mode 100644 index 0000000..c9ebc0a --- /dev/null +++ b/net/loginsrv.py @@ -0,0 +1,109 @@ +from construct import * +from construct.protocols.layer3.ipv4 import IpAddress +import charserv +from protocol import * +from utils import * +from common import * +from loggers import netlog + +server = None + + +@extendable +def smsg_server_version(data): + netlog.info("SMSG_SERVER_VERSION {}.{}".format(data.hi, data.lo)) + cmsg_login_register(server.username, server.password) + + +@extendable +def smsg_update_host(data): + netlog.info("SMSG_UPDATE_HOST {}".format(data.host)) + + +@extendable +def smsg_login_data(data): + netlog.info("SMSG_LOGIN_DATA {}".format(data)) + server.close() + + charserv.connect(data.worlds[0].address, data.worlds[0].port) + charserv.server.char_name = server.char_name + charserv.cmsg_char_server_connect(data.account, data.session1, + data.session2, 1, data.gender) + + +@extendable +def smsg_login_error(data): + error_codes = { + 0: "Unregistered ID", + 1: "Wrong password", + 2: "Account expired", + 3: "Rejected from server", + 4: "Permban", + 5: "Client too old", + 6: "Temporary ban until {}".format(data.date), + 7: "Server overpopulated", + 9: "Username already taken", + 10: "Wrong name", + 11: "Incurrect email", + 99: "Username permanently erased" } + + err_msg = "SMSG_LOGIN_ERROR {}".format( + error_codes.get(data.code, "Unknown error")) + netlog.error(err_msg) + server.close() + raise Exception(err_msg) + + +protodef = { + 0x7531 : (smsg_server_version, + Struct("data", + ULInt32("hi"), + ULInt32("lo"))), + 0x0063 : (smsg_update_host, + Struct("data", + ULInt16("length"), + StringZ("host", + lambda ctx: ctx.length - 4))), + 0x0069 : (smsg_login_data, + Struct("data", + ULInt16("length"), + ULInt32("session1"), + ULInt32("account"), + ULInt32("session2"), + IpAddress("oldip"), + StringZ("lastlogin", 24), + Padding(2), + Gender("gender"), + Array(lambda ctx: (ctx.length - 47) / 32, + Struct("worlds", + IpAddress("address"), + ULInt16("port"), + StringZ("name", 20), + ULInt16("onlineusers"), + Padding(4))))), + + 0x006a : (smsg_login_error, + Struct("data", + Byte("code"), + StringZ("date", 20))) +} + + +def cmsg_server_version_request(): + netlog.info("CMSG_SERVER_VERSION_REQUEST") + ULInt16("opcode").build_stream(0x7530, server) + + +def cmsg_login_register(username, password): + netlog.info("CMSG_LOGIN_REGISTER username={} password={}".format( + username, password)) + send_packet(server, CMSG_LOGIN_REGISTER, + (ULInt32("clientversion"), 6), + (StringZ("username", 24), username), + (StringZ("password", 24), password), + (Byte("flags"), 3)) + + +def connect(host, port): + global server + server = SocketWrapper(host=host, port=port, protodef=protodef) diff --git a/net/loginsrv.pyc b/net/loginsrv.pyc Binary files differnew file mode 100644 index 0000000..eb52b86 --- /dev/null +++ b/net/loginsrv.pyc diff --git a/net/mapserv.py b/net/mapserv.py new file mode 100644 index 0000000..121c3a9 --- /dev/null +++ b/net/mapserv.py @@ -0,0 +1,1216 @@ + +import atexit +import time +from construct import * +from construct.protocols.layer3.ipv4 import IpAddress +from protocol import * +from common import * +from utils import * +from being import BeingsCache +from item import FloorItem +from inventory import (add_to_inventory, remove_from_inventory, + add_to_storage, remove_from_storage) +from trade import reset_trade_state +from loggers import netlog + +server = None +beings_cache = None +party_info = [] +party_members = {} +player_pos = {'map': 'unknown', 'x': 0, 'y': 0, 'dir': 0} +tick = 0 +last_whisper = {'to': '', 'msg': ''} +player_inventory = {} +player_storage = {} +player_stats = {} +player_skills = {} +player_money = 0 +player_attack_range = 0 +trade_state = {'items_give': [], 'items_get': [], + 'zeny_give': 0, 'zeny_get': 0} +floor_items = {} + +for s in range(255): + player_stats[s] = 0 + + +# -------------------------------------------------------------------- +def smsg_ignore(data): + pass + + +@extendable +def smsg_being_chat(data): + cached_name = beings_cache.findName(data.id) + real_name, _ = data.message.split(' : ', 1) + if real_name != cached_name: + cmsg_name_request(data.id) + netlog.info("SMSG_BEING_CHAT id={} msg={}".format(data.id, data.message)) + + +@extendable +def smsg_being_emotion(data): + beings_cache.findName(data.id) + netlog.info("SMSG_BEING_EMOTION {} : {}".format(data.id, data.emote)) + + +@extendable +def smsg_being_move(data): + global tick + tick = data.tick + beings_cache.findName(data.id, data.job) + beings_cache[data.id].x = data.coor_pair.dst_x + beings_cache[data.id].y = data.coor_pair.dst_y + netlog.info("SMSG_BEING_MOVE {}".format(data)) + + +@extendable +def smsg_being_action(data): + global tick + tick = data.tick + netlog.info("SMSG_BEING_ACTION {}".format(data)) + + +@extendable +def smsg_being_change_direction(data): + netlog.info("SMSG_BEING_CHANGE_DIRECTION {}".format(data)) + + +@extendable +def smsg_being_name_response(data): + try: + beings_cache[data.id].name = data.name + except KeyError: + pass + netlog.info("SMSG_BEING_NAME_RESPONSE id={} name={}".format( + data.id, data.name)) + + +@extendable +def smsg_being_remove(data): + try: + del beings_cache[data.id] + except KeyError: + pass + netlog.info("SMSG_BEING_REMOVE (id={}, deadflag={})".format( + data.id, data.deadflag)) + + +@extendable +def smsg_being_visible(data): + beings_cache.findName(data.id, data.job) + beings_cache[data.id].speed = data.speed + beings_cache[data.id].x = data.coor.x + beings_cache[data.id].y = data.coor.y + netlog.info("SMSG_BEING_VISIBLE {}".format(data)) + + +@extendable +def smsg_player_chat(data): + netlog.info("SMSG_PLAYER_CHAT {}".format(data.message)) + + +@extendable +def smsg_player_equipment(data): + netlog.info("SMSG_PLAYER_EQUIPMENT {}".format(data)) + for item in data.equipment: + player_inventory[item.index] = (item.id, 1) + + +@extendable +def smsg_player_inventory(data): + netlog.info("SMSG_PLAYER_INVENTORY {}".format(data)) + for item in data.inventory: + player_inventory[item.index] = (item.id, item.amount) + + +@extendable +def smsg_player_inventory_add(data): + netlog.info("SMSG_PLAYER_INVENTORY_ADD index={} id={} amount={}".format( + data.index, data.id, data.amount)) + add_to_inventory(data.index, data.id, data.amount) + + +@extendable +def smsg_player_inventory_remove(data): + netlog.info("SMSG_PLAYER_INVENTORY_REMOVE index={} amount={}".format( + data.index, data.amount)) + remove_from_inventory(data.index, data.amount) + + +@extendable +def smsg_player_inventory_use(data): + netlog.info("SMSG_PLAYER_INVENTORY_USE {}".format(data)) + if data.amount > 0: + player_inventory[data.index] = (data.item_id, data.amount) + else: + del player_inventory[data.index] + + +@extendable +def smsg_player_move(data): + netlog.info("SMSG_PLAYER_MOVE {}".format(data)) + global tick + tick = data.tick + beings_cache.findName(data.id, data.job) + beings_cache[data.id].x = data.coor_pair.dst_x + beings_cache[data.id].y = data.coor_pair.dst_y + + +@extendable +def smsg_player_stop(data): + netlog.info("SMSG_PLAYER_STOP id={} x={} y={}".format( + data.id, data.x, data.y)) + beings_cache.findName(data.id) + beings_cache[data.id].x = data.x + beings_cache[data.id].y = data.y + + +@extendable +def smsg_player_update(data): + netlog.info("SMSG_PLAYER_UPDATE_ {}".format(data)) + beings_cache.findName(data.id, data.job) + beings_cache[data.id].speed = data.speed + beings_cache[data.id].x = data.coor.x + beings_cache[data.id].y = data.coor.y + + +@extendable +def smsg_player_warp(data): + netlog.info("SMSG_PLAYER_WARP (map={}, x={}, y={}".format( + data.map, data.x, data.y)) + player_pos['map'] = data.map + player_pos['x'] = data.x + player_pos['y'] = data.y + beings_cache.clear() + + +@extendable +def smsg_ip_response(data): + netlog.info("SMSG_IP_RESPONSE id={} ip={}".format(data.id, data.ip)) + + +@extendable +def smsg_connection_problem(data): + error_codes = { + 0 : "Authentification failed", + 1 : "No servers available", + 2 : "Account already in use", + 3 : "Speed hack detected", + 8 : "Duplicated login", + } + msg = error_codes.get(data.code, 'code ' + str(data.code)) + netlog.error("SMSG_CONNECTION_PROBLEM %d", data.code) + raise Exception(msg) + + +@extendable +def smsg_gm_chat(data): + netlog.info("SMSG_GM_CHAT {}".format(data.message)) + + +@extendable +def smsg_party_info(data): + global party_info, party_members + party_info = data + for m in data.members: + party_members[m.id] = m.nick + netlog.info("SMSG_PARTY_INFO {}".format(data)) + + +@extendable +def smsg_party_chat(data): + netlog.info("SMSG_PARTY_CHAT {} : {}".format(data.id, data.message)) + + +@extendable +def smsg_trade_request(data): + netlog.info("SMSG_TRADE_REQUEST {}".format(data.nick)) + # cmsg_trade_response("DECLINE") + + +@extendable +def smsg_trade_response(data): + netlog.info("SMSG_TRADE_RESPONSE {}".format(data.code)) + + +@extendable +def smsg_trade_item_add(data): + netlog.info("SMSG_TRADE_ITEM_ADD id={} amount={}".format( + data.id, data.amount)) + if data.id == 0: + trade_state['zeny_get'] = data.amount + else: + trade_state['items_get'].append((data.id, data.amount)) + + +@extendable +def smsg_trade_item_add_response(data): + netlog.info(("SMSG_TRADE_ITEM_ADD_RESPONSE" + " index={} amount={} code={}").format( + data.index, data.amount, data.code)) + + index = data.index + amount = data.amount + code = data.code + + if code == 0 and amount > 0: + if index > 0: + item_id, _ = player_inventory[index] + remove_from_inventory(index, amount) + trade_state['items_give'].append((item_id, amount)) + elif index == 0: + trade_state['zeny_give'] = amount + + +@extendable +def smsg_trade_cancel(data): + netlog.info("SMSG_TRADE_CANCEL") + reset_trade_state(trade_state) + + +@extendable +def smsg_trade_ok(data): + netlog.info("SMSG_TRADE_OK who={}".format(data.who)) + + +@extendable +def smsg_trade_complete(data): + netlog.info("SMSG_TRADE_COMPLETE") + global player_money + player_money += trade_state['zeny_get'] - trade_state['zeny_give'] + # reset_trade_state(trade_state) + + +@extendable +def smsg_whisper(data): + netlog.info("SMSG_WHISPER {} : {}".format(data.nick, data.message)) + + +@extendable +def smsg_whisper_response(data): + m = {0: "OK", 1: "Recepient is offline"} + netlog.info("SMSG_WHISPER_RESPONSE {}".format(m.get(data.code, "error"))) + + +@extendable +def smsg_server_ping(data): + global tick + tick = data.tick + netlog.info("SMSG_SERVER_PING tick={}".format(data.tick)) + + +@extendable +def smsg_map_login_success(data): + netlog.info("SMSG_MAP_LOGIN_SUCCESS {}".format(data)) + global tick + tick = data.tick + player_pos['x'] = data.coor.x + player_pos['y'] = data.coor.y + player_pos['dir'] = data.coor.dir + + +@extendable +def smsg_walk_response(data): + global tick + tick = data.tick + player_pos['x'] = data.coor_pair.dst_x + player_pos['y'] = data.coor_pair.dst_y + netlog.info("SMSG_WALK_RESPONSE {}".format(data)) + + +@extendable +def smsg_item_visible(data): + netlog.info("SMSG_ITEM_VISIBLE {}".format(data)) + item = FloorItem(data.id, data.type, data.amount, data.x, data.y) + floor_items[data.id] = item + + +@extendable +def smsg_item_dropped(data): + netlog.info("SMSG_ITEM_DROPPED {}".format(data)) + item = FloorItem(data.id, data.type, data.amount, data.x, data.y) + floor_items[data.id] = item + + +@extendable +def smsg_item_remove(data): + netlog.info("SMSG_ITEM_REMOVE id={}".format(data.id)) + if data.id in floor_items: + del floor_items[data.id] + + +@extendable +def smsg_player_stat_update_x(data): + netlog.info("SMSG_PLAYER_STAT_UPDATE_X type={} value={}".format( + data.type, data.stat_value)) + player_stats[data.type] = data.stat_value + + +@extendable +def smsg_being_self_effect(data): + netlog.info("SMSG_BEING_SELF_EFFECT id={} effect={}".format( + data.id, data.effect)) + + +@extendable +def smsg_being_status_change(data): + netlog.info("SMSG_BEING_STATUS_CHANGE id={} status={} flag={}".format( + data.id, data.status, data.flag)) + + +@extendable +def smsg_player_status_change(data): + netlog.info("SMSG_PLAYER_STATUS_CHANGE {}".format(data)) + + +@extendable +def smsg_npc_message(data): + netlog.info("SMSG_NPC_MESSAGE id={} message={}".format( + data.id, data.message)) + + +@extendable +def smsg_npc_choice(data): + netlog.info("SMSG_NPC_CHOICE {}".format(data.select)) + + +@extendable +def smsg_npc_close(data): + netlog.info("SMSG_NPC_CLOSE id={}".format(data.id)) + + +@extendable +def smsg_npc_next(data): + netlog.info("SMSG_NPC_NEXT id={}".format(data.id)) + + +@extendable +def smsg_npc_int_input(data): + netlog.info("SMSG_NPC_INT_INPUT id={}".format(data.id)) + + +@extendable +def smsg_npc_str_input(data): + netlog.info("SMSG_NPC_STR_INPUT id={}".format(data.id)) + + +@extendable +def smsg_npc_command(data): + netlog.info("SMSG_NPC_COMMAND {}".format(data)) + + +@extendable +def smsg_npc_buy(data): + netlog.info("SMSG_NPC_BUY {}".format(data)) + + +@extendable +def smsg_player_skills(data): + netlog.info("SMSG_PLAYER_SKILLS {}".format(data)) + for skill in data.skills: + player_skills[skill.id] = skill.level + + +@extendable +def smsg_player_skill_up(data): + netlog.info("SMSG_PLAYER_SKILL_UP {}".format(data)) + player_skills.setdefault(data.id, data.level) + + +@extendable +def smsg_storage_status(data): + netlog.info("SMSG_STORAGE_STATUS used={} max_size={}".format( + data.used, data.max_size)) + + +@extendable +def smsg_storage_add(data): + netlog.info("SMSG_STORAGE_ADD {}".format(data)) + add_to_storage(data.index, data.id, data.amount) + + +@extendable +def smsg_storage_remove(data): + netlog.info("SMSG_STORAGE_REMOVE {}".format(data)) + remove_from_storage(data.index, data.amount) + + +@extendable +def smsg_storage_close(data): + netlog.info("SMSG_STORAGE_CLOSE") + + +@extendable +def smsg_storage_equip(data): + netlog.info("SMSG_STORAGE_EQUIP {}".format(data)) + for item in data.equipment: + player_storage[item.index] = (item.id, 1) + + +@extendable +def smsg_storage_items(data): + netlog.info("SMSG_STORAGE_ITEMS {}".format(data)) + for item in data.storage: + player_storage[item.index] = (item.id, item.amount) + + +@extendable +def smsg_player_attack_range(data): + netlog.info("SMSG_PLAYER_ATTACK_RANGE %d", data.range) + global player_attack_range + player_attack_range = data.range + + +@extendable +def smsg_player_arrow_message(data): + netlog.info("SMSG_PLAYER_ARROW_MESSAGE %d", data.code) + + +# -------------------------------------------------------------------- +protodef = { + 0x8000 : (smsg_ignore, Field("data", 2)), # ignore + 0x008a : (smsg_being_action, + Struct("data", + ULInt32("src_id"), + ULInt32("dst_id"), + ULInt32("tick"), + ULInt32("src_speed"), + ULInt32("dst_speed"), + ULInt16("damage"), + Padding(2), + Byte("type"), + Padding(2))), + 0x009c : (smsg_being_change_direction, + Struct("data", + ULInt32("id"), + Padding(2), + Byte("dir"))), + 0x00c3 : (smsg_ignore, Field("data", 6)), # being-change-looks + 0x01d7 : (smsg_ignore, Field("data", 9)), # being-change-looks2 + 0x008d : (smsg_being_chat, + Struct("data", + ULInt16("length"), + ULInt32("id"), + StringZ("message", lambda ctx: ctx.length - 8))), + 0x00c0 : (smsg_being_emotion, + Struct("data", + ULInt32("id"), + Byte("emote"))), + 0x007b : (smsg_being_move, + Struct("data", + ULInt32("id"), + ULInt16("speed"), + Padding(6), + ULInt16("job"), + Padding(6), + ULInt32("tick"), + Padding(10), + ULInt32("hp"), + ULInt32("max_hp"), + # Probe("debug", show_stream=False, show_stack=False), + Padding(6), + BitStruct("coor_pair", + BitField("src_x", 10), + BitField("src_y", 10), + BitField("dst_x", 10), + BitField("dst_y", 10)), + Padding(5))), + 0x0086 : (smsg_being_move, + Struct("data", + ULInt32("id"), + # Padding(10) + BitStruct("coor_pair", + BitField("src_x", 10), + BitField("src_y", 10), + BitField("dst_x", 10), + BitField("dst_y", 10)), + ULInt32("tick"))), + 0x0095 : (smsg_being_name_response, + Struct("data", + ULInt32("id"), + StringZ("name", 24))), + 0x0080 : (smsg_being_remove, + Struct("data", + ULInt32("id"), + Byte("deadflag"))), + 0x0148 : (smsg_ignore, Field("data", 6)), # being-resurrect + 0x019b : (smsg_being_self_effect, + Struct("data", + ULInt32("id"), + ULInt32("effect"))), + 0x007c : (smsg_ignore, Field("data", 39)), # spawn + 0x0196 : (smsg_being_status_change, + Struct("data", + ULInt16("status"), + ULInt32("id"), + Flag("flag"))), + 0x0078 : (smsg_being_visible, + Struct("data", + ULInt32("id"), + ULInt16("speed"), + Padding(6), + ULInt16("job"), + Padding(16), + ULInt32("hp"), + ULInt32("max_hp"), + Padding(6), + BitStruct("coor", + BitField("x", 10), + BitField("y", 10), + Nibble("dir")), + Padding(5))), + 0x013c : (smsg_ignore, Field("data", 2)), # arrow-equip + 0x013b : (smsg_player_arrow_message, + Struct("data", + ULInt16("code"))), + 0x013a : (smsg_player_attack_range, + Struct("data", + ULInt16("range"))), + 0x008e : (smsg_player_chat, + Struct("data", + ULInt16("length"), + StringZ("message", lambda ctx: ctx.length - 4))), + 0x00aa : (smsg_ignore, # player-equip + Struct("data", + ULInt16("index"), + ULInt16("type"), + Byte("flag"))), + 0x00a4 : (smsg_player_equipment, + Struct("data", + ULInt16("length"), + Array(lambda ctx: (ctx.length - 4) / 20, + Struct("equipment", + ULInt16("index"), + ULInt16("id"), + Padding(16))))), + 0x0195 : (smsg_ignore, Field("data", 100)), # guild-party-info + 0x01ee : (smsg_player_inventory, + Struct("data", + ULInt16("length"), + Array(lambda ctx: (ctx.length - 4) / 18, + Struct("inventory", + ULInt16("index"), + ULInt16("id"), + Padding(2), + ULInt16("amount"), + Padding(10))))), + 0x00a0 : (smsg_player_inventory_add, + Struct("data", + ULInt16("index"), + ULInt16("amount"), + ULInt16("id"), + Padding(15))), + 0x00af : (smsg_player_inventory_remove, + Struct("data", + ULInt16("index"), + ULInt16("amount"))), + 0x01c8 : (smsg_player_inventory_use, + Struct("data", + ULInt16("index"), + ULInt16("item_id"), + Padding(4), + ULInt16("amount"), + Byte("type"))), + 0x01da : (smsg_player_move, + Struct("data", + ULInt32("id"), + ULInt16("speed"), + Padding(6), + ULInt16("job"), + Padding(8), + ULInt32("tick"), + Padding(22), + BitStruct("coor_pair", + BitField("src_x", 10), + BitField("src_y", 10), + BitField("dst_x", 10), + BitField("dst_y", 10)), + Padding(5))), + 0x0139 : (smsg_ignore, Field("data", 14)), # player-move-to-attack + 0x010f : (smsg_player_skills, + Struct("data", + ULInt16("length"), + Array(lambda ctx: (ctx.length - 4) / 37, + Struct("skills", + ULInt16("id"), + Padding(4), + ULInt16("level"), + Padding(29))))), + 0x010e : (smsg_player_skill_up, + Struct("data", + ULInt16("id"), + ULInt16("level"), + Padding(5))), + 0x00b0 : (smsg_player_stat_update_x, + Struct("data", + ULInt16("type"), + ULInt32("stat_value"))), + 0x00b1 : (smsg_player_stat_update_x, + Struct("data", + ULInt16("type"), + ULInt32("stat_value"))), + 0x0141 : (smsg_player_stat_update_x, + Struct("data", + ULInt32("type"), + ULInt32("stat_value"), + ULInt32("bonus"))), + 0x00bc : (smsg_player_stat_update_x, + Struct("data", + ULInt16("type"), + Flag("ok"), + Byte("stat_value"))), + 0x00bd : (smsg_ignore, Field("data", 42)), # player-stat-update-5 + 0x00be : (smsg_player_stat_update_x, + Struct("data", + ULInt16("type"), + Byte("stat_value"))), + 0x0119 : (smsg_player_status_change, + Struct("data", + ULInt32("id"), + ULInt16("stun"), + ULInt16("effect"), + ULInt16("effect_hi"), + Padding(1))), + 0x0088 : (smsg_player_stop, + Struct("data", + ULInt32("id"), + ULInt16("x"), + ULInt16("y"))), + 0x00ac : (smsg_ignore, Field("data", 5)), # player-unequip + 0x01d8 : (smsg_player_update, + Struct("data", + ULInt32("id"), + ULInt16("speed"), + Padding(6), + ULInt16("job"), + Padding(30), + BitStruct("coor", + BitField("x", 10), + BitField("y", 10), + Nibble("dir")), + Padding(5))), + 0x01d9 : (smsg_player_update, + Struct("data", + ULInt32("id"), + ULInt16("speed"), + Padding(6), + ULInt16("job"), + Padding(30), + BitStruct("coor", + BitField("x", 10), + BitField("y", 10), + Nibble("dir")), + Padding(4))), + 0x0091 : (smsg_player_warp, + Struct("data", + StringZ("map", 16), + ULInt16("x"), + ULInt16("y"))), + 0x0020 : (smsg_ip_response, + Struct("data", + ULInt32("id"), + IpAddress("ip"))), + 0x019a : (smsg_ignore, Field("data", 12)), # pvp-set + 0x0081 : (smsg_connection_problem, + Struct("data", Byte("code"))), + 0x009a : (smsg_gm_chat, + Struct("data", + ULInt16("length"), + StringZ("message", lambda ctx: ctx.length - 4))), + 0x009e : (smsg_item_dropped, + Struct("data", + ULInt32("id"), + ULInt16("type"), + Byte("identify"), + ULInt16("x"), + ULInt16("y"), + Byte("sub_x"), + Byte("sub_y"), + ULInt16("amount"))), + 0x00a1 : (smsg_item_remove, + Struct("data", ULInt32("id"))), + 0x009d : (smsg_item_visible, + Struct("data", + ULInt32("id"), + ULInt16("type"), + Byte("identify"), + ULInt16("x"), + ULInt16("y"), + ULInt16("amount"), + Byte("sub_x"), + Byte("sub_y"))), + 0x00fb : (smsg_party_info, + Struct("data", + ULInt16("length"), + StringZ("name", 24), + Array(lambda ctx: (ctx.length - 28) / 46, + Struct("members", + ULInt32("id"), + StringZ("nick", 24), + StringZ("map", 16), + Flag("leader"), + Flag("online"))))), + 0x00fe : (smsg_ignore, Field("data", 28)), # party-invited + 0x0107 : (smsg_ignore, Field("data", 8)), # party-update-coords + 0x0106 : (smsg_ignore, Field("data", 8)), # party-update-hp + 0x0109 : (smsg_party_chat, + Struct("data", + ULInt16("length"), + ULInt32("id"), + # Probe("debug", show_stream=False, show_stack=False), + StringZ("message", lambda ctx: ctx.length - 8))), + 0x01b9 : (smsg_ignore, Field("data", 4)), # skill-cast-cancel + 0x013e : (smsg_ignore, Field("data", 22)), # skill-casting + 0x01de : (smsg_ignore, Field("data", 31)), # skill-damage + 0x011a : (smsg_ignore, Field("data", 13)), # skill-no-damage + 0x00e5 : (smsg_trade_request, + Struct("data", + StringZ("nick", 24))), + 0x00e7 : (smsg_trade_response, + Struct("data", Byte("code"))), + 0x00e9 : (smsg_trade_item_add, + Struct("data", + ULInt32("amount"), + ULInt16("id"), + Padding(11))), + 0x01b1 : (smsg_trade_item_add_response, + Struct("data", + ULInt16("index"), + ULInt16("amount"), + Byte("code"))), + 0x00ee : (smsg_trade_cancel, + StaticField("data", 0)), + 0x00f0 : (smsg_trade_complete, + StaticField("data", 1)), + 0x00ec : (smsg_trade_ok, + Struct("data", Byte("who"))), + 0x0097 : (smsg_whisper, + Struct("data", + ULInt16("length"), + StringZ("nick", 24), + StringZ("message", lambda ctx: ctx.length - 28))), + 0x0098 : (smsg_whisper_response, + Struct("data", Byte("code"))), + 0x0073 : (smsg_map_login_success, + Struct("data", + ULInt32("tick"), + # ULInt24("coor"), + BitStruct("coor", + BitField("x", 10), + BitField("y", 10), + Nibble("dir")), + Padding(2))), + 0x007f : (smsg_server_ping, + Struct("data", + ULInt32("tick"))), + 0x0087 : (smsg_walk_response, + Struct("data", + ULInt32("tick"), + BitStruct("coor_pair", + BitField("src_x", 10), + BitField("src_y", 10), + BitField("dst_x", 10), + BitField("dst_y", 10)), + Padding(1))), + 0x00b5 : (smsg_ignore, Field("data", 6)), # npc-next + 0x00b4 : (smsg_npc_message, + Struct("data", + ULInt16("length"), + ULInt32("id"), + StringZ("message", lambda ctx: ctx.length - 8))), + 0x00b7 : (smsg_npc_choice, + Struct("data", + ULInt16("length"), + ULInt32("id"), + StringZ("select", lambda ctx: ctx.length - 8))), + 0x00b6 : (smsg_npc_close, + Struct("data", + ULInt32("id"))), + 0x00b5 : (smsg_npc_next, + Struct("data", + ULInt32("id"))), + 0x0142 : (smsg_npc_int_input, + Struct("data", + ULInt32("id"))), + 0x01d4 : (smsg_npc_str_input, + Struct("data", + ULInt32("id"))), + 0x0212 : (smsg_npc_command, + Struct("data", + ULInt32("id"), + ULInt16("command"), + ULInt32("target_id"), + ULInt16("x"), + ULInt16("y"))), + 0x00c6 : (smsg_npc_buy, + Struct("data", + ULInt16("length"), + Array(lambda ctx: (ctx.length - 4) / 11, + Struct("items", + ULInt32("price"), + Padding(4), + ULInt16("type"), + ULInt32("id"))))), + 0x00f2 : (smsg_storage_status, + Struct("data", + ULInt16("used"), + ULInt16("max_size"))), + 0x00f4 : (smsg_storage_add, + Struct("data", + ULInt16("index"), + ULInt32("amount"), + ULInt16("id"), + Padding(11))), + 0x00f6 : (smsg_storage_remove, + Struct("data", + ULInt16("index"), + ULInt32("amount"))), + 0x00f8 : (smsg_storage_close, + Struct("data")), # ????? + 0x00a6 : (smsg_storage_equip, + Struct("data", + ULInt16("length"), + Array(lambda ctx: (ctx.length - 4) / 20, + Struct("equipment", + ULInt16("index"), + ULInt16("id"), + Padding(16))))), + 0x01f0 : (smsg_storage_items, + Struct("data", + ULInt16("length"), + Array(lambda ctx: (ctx.length - 4) / 18, + Struct("storage", + ULInt16("index"), + ULInt16("id"), + Padding(2), + ULInt16("amount"), + Padding(10))))), +} + + +# -------------------------------------------------------------------- +def cmsg_map_server_connect(account, char_id, session1, session2, gender): + netlog.info(("CMSG_MAP_SERVER_CONNECT account={} char_id={} " + "session1={} session2={} gender={}").format(account, + char_id, session1, session2, gender)) + + send_packet(server, CMSG_MAP_SERVER_CONNECT, + (ULInt32("account"), account), + (ULInt32("char_id"), char_id), + (ULInt32("session1"), session1), + (ULInt32("session2"), session2), + (Gender("gender"), gender)) + + +def cmsg_map_loaded(): + netlog.info("CMSG_MAP_LOADED") + ULInt16("opcode").build_stream(CMSG_MAP_LOADED, server) + + +def cmsg_map_server_ping(tick_=-1): + netlog.info("CMSG_MAP_SERVER_PING tick={}".format(tick)) + if tick_ < 0: + tick_ = tick + 1 + send_packet(server, CMSG_MAP_SERVER_PING, + (ULInt32("tick"), tick_)) + + +def cmsg_trade_response(answer): + if answer in ("OK", "ACCEPT", "YES", True): + answer = 3 + elif answer in ("DECLINE", "CANCEL", "NO", False): + answer = 4 + + s = {3: "ACCEPT", 4: "DECLINE"} + + netlog.info("CMSG_TRADE_RESPONSE {}".format(s[answer])) + send_packet(server, CMSG_TRADE_RESPONSE, + (Byte("answer"), answer)) + + +def cmsg_trade_request(player_id): + netlog.info("CMSG_TRADE_REQUEST id={}".format(player_id)) + send_packet(server, CMSG_TRADE_REQUEST, + (ULInt32("id"), player_id)) + + +def cmsg_trade_item_add_request(index, amount): + netlog.info("CMSG_TRADE_ITEM_ADD_REQUEST index={} amount={}".format( + index, amount)) + + # Workaround for TMWA, I'm pretty sure it has a bug related to + # not sending back the amount of GP player added to trade + if index == 0 and amount <= player_money: + trade_state['zeny_give'] = amount + + send_packet(server, CMSG_TRADE_ITEM_ADD_REQUEST, + (ULInt16("index"), index), + (ULInt32("amount"), amount)) + + +def cmsg_trade_ok(): + netlog.info("CMSG_TRADE_OK") + ULInt16("opcode").build_stream(CMSG_TRADE_OK, server) + + +def cmsg_trade_add_complete(): + netlog.info("CMSG_TRADE_ADD_COMPLETE") + ULInt16("opcode").build_stream(CMSG_TRADE_ADD_COMPLETE, server) + + +def cmsg_trade_cancel_request(): + netlog.info("CMSG_TRADE_CANCEL_REQUEST") + ULInt16("opcode").build_stream(CMSG_TRADE_CANCEL_REQUEST, server) + + +def cmsg_name_request(id_): + netlog.info("CMSG_NAME_REQUEST id={}".format(id_)) + send_packet(server, CMSG_NAME_REQUEST, + (ULInt32("id"), id_)) + + +def cmsg_chat_message(msg): + netlog.info("CMSG_CHAT_MESSAGE {}".format(msg)) + l = len(msg) + send_packet(server, CMSG_CHAT_MESSAGE, + (ULInt16("len"), l + 5), + (StringZ("msg", l + 1), msg)) + + +def cmsg_chat_whisper(to_, msg): + netlog.info("CMSG_CHAT_WHISPER to {} : {}".format(to_, msg)) + last_whisper['to'] = to_ + last_whisper['msg'] = msg + l = len(msg) + send_packet(server, CMSG_CHAT_WHISPER, + (ULInt16("len"), l + 29), + (StringZ("nick", 24), to_), + (StringZ("msg", l + 1), msg)) + + +def cmsg_party_message(msg): + netlog.info("CMSG_PARTY_MESSAGE {}".format(msg)) + l = len(msg) + send_packet(server, CMSG_PARTY_MESSAGE, + (ULInt16("len"), l + 4), + (String("msg", l), msg)) + + +def cmsg_player_change_dest(x_, y_): + netlog.info("CMSG_PLAYER_CHANGE_DEST x={} y={}".format(x_, y_)) + + class C: + opcode = CMSG_PLAYER_CHANGE_DEST + + class coor: + x = x_ + y = y_ + dir = 0 + + d = Struct("packet", + ULInt16("opcode"), + BitStruct("coor", BitField("x", 10), + BitField("y", 10), Nibble("dir"))) + + d.build_stream(C, server) + + +def cmsg_player_change_dir(new_dir): + netlog.info("CMSG_PLAYER_CHANGE_DIR {}".format(new_dir)) + send_packet(server, CMSG_PLAYER_CHANGE_DIR, + (ULInt16("unused"), 0), + (ULInt8("dir"), new_dir)) + + +def cmsg_player_change_act(id_, action): + netlog.info("CMSG_PLAYER_CHANGE_ACT id={} action={}".format(id_, action)) + send_packet(server, CMSG_PLAYER_CHANGE_ACT, + (ULInt32("id"), id_), + (Byte("action"), action)) + + +def cmsg_player_respawn(): + netlog.info("CMSG_PLAYER_RESPAWN") + send_packet(server, CMSG_PLAYER_RESPAWN, + (Byte("action"), 0)) + + +def cmsg_player_emote(emote): + netlog.info("CMSG_PLAYER_EMOTE {}".format(emote)) + send_packet(server, CMSG_PLAYER_EMOTE, + (Byte("emote"), emote)) + + +def cmsg_item_pickup(item_id): + netlog.info("CMSG_ITEM_PICKUP id={}".format(item_id)) + send_packet(server, CMSG_ITEM_PICKUP, + (ULInt32("id"), item_id)) + + +def cmsg_player_stop_attack(): + netlog.info("CMSG_PLAYER_STOP_ATTACK") + ULInt16("opcode").build_stream(CMSG_PLAYER_STOP_ATTACK, server) + + +def cmsg_player_equip(index): + netlog.info("CMSG_PLAYER_EQUIP index={}".format(index)) + send_packet(server, CMSG_PLAYER_EQUIP, + (ULInt16("index"), index), + (ULInt16("unused"), 0)) + + +def cmsg_player_unequip(index): + netlog.info("CMSG_PLAYER_UNEQUIP index={}".format(index)) + send_packet(server, CMSG_PLAYER_UNEQUIP, + (ULInt16("index"), index)) + + +def cmsg_player_inventory_use(index, item_id): + netlog.info("CMSG_PLAYER_INVENTORY_USE index={} id={}".format( + index, item_id)) + send_packet(server, CMSG_PLAYER_INVENTORY_USE, + (ULInt16("index"), index), + (ULInt32("id"), item_id)) + + +def cmsg_player_inventory_drop(index, amount): + netlog.info("CMSG_PLAYER_INVENTORY_DROP index={} amount={}".format( + index, amount)) + send_packet(server, CMSG_PLAYER_INVENTORY_DROP, + (ULInt16("index"), index), + (ULInt16("amount"), amount)) + + +# --------------- NPC --------------------- +def cmsg_npc_talk(npcId): + netlog.info("CMSG_NPC_TALK id={}".format(npcId)) + send_packet(server, CMSG_NPC_TALK, + (ULInt32("npcId"), npcId), + (Byte("unused"), 0)) + + +def cmsg_npc_next_request(npcId): + netlog.info("CMSG_NPC_NEXT_REQUEST id={}".format(npcId)) + send_packet(server, CMSG_NPC_NEXT_REQUEST, + (ULInt32("npcId"), npcId)) + + +def cmsg_npc_close(npcId): + netlog.info("CMSG_NPC_CLOSE id={}".format(npcId)) + send_packet(server, CMSG_NPC_CLOSE, + (ULInt32("npcId"), npcId)) + + +def cmsg_npc_list_choice(npcId, choice): + netlog.info("CMSG_NPC_LIST_CHOICE id={} choice={}".format(npcId, choice)) + send_packet(server, CMSG_NPC_LIST_CHOICE, + (ULInt32("npcId"), npcId), + (Byte("choice"), choice)) + + +def cmsg_npc_int_response(npcId, value): + netlog.info("CMSG_NPC_INT_RESPONSE id={} value={}".format(npcId, value)) + send_packet(server, CMSG_NPC_INT_RESPONSE, + (ULInt32("npcId"), npcId), + (SLInt32("value"), value)) + + +def cmsg_npc_str_response(npcId, value): + netlog.info("CMSG_NPC_STR_RESPONSE id={} value={}".format(npcId, value)) + l = len(value) + send_packet(server, CMSG_NPC_STR_RESPONSE, + (ULInt16("len"), l + 9), + (ULInt32("npcId"), npcId), + (StringZ("value", l + 1), value)) + + +def cmsg_npc_buy_sell_request(npcId, action): + netlog.info("CMSG_NPC_BUY_SELL_REQUEST id={} action={}".format( + npcId, action)) + send_packet(server, CMSG_NPC_BUY_SELL_REQUEST, + (ULInt32("npcId"), npcId), + (Byte("action"), action)) + + +def cmsg_npc_buy_request(itemId, amount): + netlog.info("CMSG_NPC_BUY_REQUEST itemId={} amount={}".format( + itemId, amount)) + send_packet(server, CMSG_NPC_BUY_REQUEST, + (ULInt16("len"), 8), + (ULInt16("amount"), amount), + (ULInt16("itemId"), itemId)) + + +def cmsg_npc_sell_request(index, amount): + netlog.info("CMSG_NPC_SELL_REQUEST index={} amount={}".format( + index, amount)) + send_packet(server, CMSG_NPC_SELL_REQUEST, + (ULInt16("len"), 8), + (ULInt16("index"), index), + (ULInt16("amount"), amount)) + + +# --------------- STATS, SKILLS -------------------------- +def cmsg_stat_update_request(stat, value): + netlog.info("CMSG_STAT_UPDATE_REQUEST stat={} value={}".format( + stat, value)) + send_packet(server, CMSG_STAT_UPDATE_REQUEST, + (ULInt16("stat"), stat), + (Byte("value"), value)) + + +def cmsg_skill_levelup_request(skillId): + netlog.info("CMSG_SKILL_LEVELUP_REQUEST skillId={}".format(skillId)) + send_packet(server, CMSG_SKILL_LEVELUP_REQUEST, + (ULInt16("id"), skillId)) + + +# ------------------- STORAGE ------------------------------- +def cmsg_move_to_storage(index, amount): + netlog.info("CMSG_MOVE_TO_STORAGE index={} amount={}".format( + index, amount)) + send_packet(server, CMSG_MOVE_TO_STORAGE, + (ULInt16("index"), index), + (ULInt32("amount"), amount)) + + +def cmsg_move_from_storage(index, amount): + netlog.info("CMSG_MOVE_FROM_STORAGE index={} amount={}".format( + index, amount)) + send_packet(server, CMSG_MOVE_FROM_STORAGE, + (ULInt16("index"), index), + (ULInt32("amount"), amount)) + + +def cmsg_storage_close(): + netlog.info("CMSG_CLOSE_STORAGE") + ULInt16("opcode").build_stream(CMSG_CLOSE_STORAGE, server) + + +# -------------------------------------------------------------------- + +last_ping_ts = 0 + + +def connect(host, port): + + def ping_logic(ts): + global last_ping_ts + if last_ping_ts and ts > last_ping_ts + 15: + last_ping_ts = time.time() + cmsg_map_server_ping() + + global server, beings_cache + beings_cache = BeingsCache(cmsg_name_request) + #server = SocketWrapper(host=host, port=port, protodef=protodef) + server = SocketWrapper(host="52.174.196.146", port=port, protodef=protodef) + + import logicmanager + logicmanager.logic_manager.add_logic(ping_logic) + + +@atexit.register +def cleanup(): + if server is not None: + server.close() diff --git a/net/mapserv.pyc b/net/mapserv.pyc Binary files differnew file mode 100644 index 0000000..0d7c757 --- /dev/null +++ b/net/mapserv.pyc diff --git a/net/onlineusers.py b/net/onlineusers.py new file mode 100644 index 0000000..92b5b0f --- /dev/null +++ b/net/onlineusers.py @@ -0,0 +1,86 @@ + +""" +Copyright 2015-2016, Joseph Botosh <rumly111@gmail.com> + +This file is part of tradey, a trading bot in The Mana World +see www.themanaworld.org + +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, see <http://www.gnu.org/licenses/>. + +Additionally to the GPL, you are *strongly* encouraged to share any +modifications you do on these sources. +""" + +import urllib2 +import string +import threading +import time + +from common import netlog + + +class OnlineUsers(threading.Thread): + + def __init__(self, online_url='https://server.themanaworld.org/online-old.txt', + update_interval=60, refresh_hook=None): + self._active = True + self._timer = 0 + self._url = online_url + self._update_interval = update_interval + self.__lock = threading.Lock() + self.__online_users = [] + self.refresh_hook = refresh_hook + threading.Thread.__init__(self) + + @property + def online_users(self): + self.__lock.acquire(True) + users = self.__online_users[:] + self.__lock.release() + return users + + @staticmethod + def dl_online_list(url): + """ + Download online.txt, parse it, and return a list of online user nicks. + If error occurs, return empty list + """ + try: + data = urllib2.urlopen(url).read() + except urllib2.URLError, e: + netlog.error("urllib error: %s", e.message) + return [] + start = string.find(data, '------------------------------\n') + 31 + end = string.rfind(data, '\n\n') + s = data[start:end] + return map(lambda n: n[:-5].strip() if n.endswith('(GM) ') + else n.strip(), s.split('\n')) + + def run(self): + while self._active: + if (time.time() - self._timer) > self._update_interval: + users = self.dl_online_list(self._url) + + if self.refresh_hook: + self.refresh_hook(set(users), set(self.__online_users)) + + self.__lock.acquire(True) + self.__online_users = users + self.__lock.release() + self._timer = time.time() + else: + time.sleep(1.0) + + def stop(self): + self._active = False diff --git a/net/onlineusers.pyc b/net/onlineusers.pyc Binary files differnew file mode 100644 index 0000000..f22c505 --- /dev/null +++ b/net/onlineusers.pyc diff --git a/net/packetlen.py b/net/packetlen.py new file mode 100644 index 0000000..4c26271 --- /dev/null +++ b/net/packetlen.py @@ -0,0 +1,28 @@ +# autogenerated file from manaplus sources. + +packet_lengths = { + 0x0062: 3, 0x0063: -1, 0x0069: -1, 0x006a: 23, 0x006b: -1, 0x006c: 3, + 0x006d: 108, 0x006e: 3, 0x006f: 2, 0x0070: 3, 0x0071: 28, 0x0073: 11, + 0x0078: 54, 0x007b: 60, 0x007c: 41, 0x007f: 6, 0x0080: 7, 0x0081: 3, + 0x0086: 16, 0x0087: 12, 0x0088: 10, 0x008a: 29, 0x008d: -1, 0x008e: -1, + 0x0091: 22, 0x0092: 28, 0x0095: 30, 0x0097: -1, 0x0098: 3, 0x009a: -1, + 0x009c: 9, 0x009d: 17, 0x009e: 17, 0x00a0: 23, 0x00a1: 6, 0x00a4: -1, + 0x00a6: -1, 0x00a8: 7, 0x00aa: 7, 0x00ac: 7, 0x00af: 6, 0x00b0: 8, + 0x00b1: 8, 0x00b3: 3, 0x00b4: -1, 0x00b5: 6, 0x00b6: 6, 0x00b7: -1, + 0x00bc: 6, 0x00bd: 44, 0x00be: 5, 0x00c0: 7, 0x00c2: 6, 0x00c3: 8, + 0x00c4: 6, 0x00c6: -1, 0x00c7: -1, 0x00ca: 3, 0x00cb: 3, 0x00cd: 6, + 0x00d2: 4, 0x00e5: 26, 0x00e7: 3, 0x00e9: 19, 0x00ec: 3, 0x00ee: 2, + 0x00f0: 3, 0x00f2: 6, 0x00f4: 21, 0x00f6: 8, 0x00f8: 2, 0x00fa: 3, + 0x00fb: -1, 0x00fd: 27, 0x00fe: 30, 0x0101: 6, 0x0104: 79, 0x0105: 31, + 0x0106: 10, 0x0107: 10, 0x0109: -1, 0x010c: 6, 0x010e: 11, 0x010f: -1, + 0x0110: 10, 0x0119: 13, 0x011a: 15, 0x0139: 16, 0x013a: 4, 0x013b: 4, + 0x013c: 4, 0x013e: 24, 0x0141: 14, 0x0142: 6, 0x0148: 8, 0x014c: -1, + 0x014e: 6, 0x0152: -1, 0x0154: -1, 0x0156: -1, 0x015a: 66, 0x015c: 90, + 0x015e: 6, 0x0160: -1, 0x0162: -1, 0x0163: -1, 0x0166: -1, 0x0167: 3, + 0x0169: 3, 0x016a: 30, 0x016c: 43, 0x016d: 14, 0x016f: 182, 0x0171: 30, + 0x0173: 3, 0x0174: -1, 0x017f: -1, 0x0181: 3, 0x0184: 10, 0x018b: 4, + 0x0194: 30, 0x0195: 102, 0x0196: 9, 0x0199: 4, 0x019a: 14, 0x019b: 10, + 0x01b1: 7, 0x01b6: 114, 0x01b9: 6, 0x01c8: 13, 0x01d4: 6, 0x01d7: 11, + 0x01d8: 54, 0x01d9: 53, 0x01da: 60, 0x01de: 33, 0x01ee: -1, 0x01f0: -1, + 0x020c: 10, 0x0212: 16, 0x0214: 8, 0x0215: -1, 0x0225: -1, 0x0226: 10, + 0x0227: -1, 0x0228: -1, 0x0229: -1, 0x0230: -1, 0x7531: 10, } diff --git a/net/packetlen.pyc b/net/packetlen.pyc Binary files differnew file mode 100644 index 0000000..843e137 --- /dev/null +++ b/net/packetlen.pyc diff --git a/net/protocol.py b/net/protocol.py new file mode 100644 index 0000000..14fb462 --- /dev/null +++ b/net/protocol.py @@ -0,0 +1,105 @@ +SMSG_UPDATE_HOST = 0x0063 +SMSG_LOGIN_DATA = 0x0069 +SMSG_LOGIN_ERROR = 0x006a +SMSG_CHAR_LOGIN = 0x006b +SMSG_CHAR_LOGIN_ERROR = 0x006c +SMSG_QUEST_PLAYER_VARS = 0x0215 +SMSG_CHAR_MAP_INFO = 0x0071 +SMSG_MAP_LOGIN_SUCCESS = 0x0073 +CMSG_CHAR_SERVER_CONNECT = 0x0065 +CMSG_CHAR_SELECT = 0x0066 +CMSG_MAP_SERVER_CONNECT = 0x0072 +CMSG_CHAT_WHISPER = 0x0096 +CMSG_CHAT_MESSAGE = 0x008c +CMSG_MAP_LOADED = 0x007d +CMSG_CLIENT_QUIT = 0x018A + +SMSG_WHISPER = 0x0097 +SMSG_BEING_CHAT = 0x008d + +SMSG_PLAYER_CHAT = 0x008e +CMSG_PLAYER_CHANGE_ACT = 0x0089 +CMSG_PLAYER_RESPAWN = 0x00b2 + +SMSG_PLAYER_INVENTORY = 0x01ee +SMSG_PLAYER_INVENTORY_ADD = 0x00a0 +SMSG_PLAYER_INVENTORY_REMOVE = 0x00af +SMSG_PLAYER_EQUIPMENT = 0x00a4 +SMSG_PLAYER_STAT_UPDATE_1 = 0x00b0 +SMSG_PLAYER_STAT_UPDATE_2 = 0x00b1 + +SMSG_BEING_VISIBLE = 0x0078 +SMSG_BEING_MOVE = 0x007b +SMSG_BEING_REMOVE = 0x0080 +SMSG_PLAYER_MOVE = 0x01da +SMSG_PLAYER_WARP = 0x0091 +SMSG_PLAYER_UPDATE_1 = 0x01d8 +SMSG_PLAYER_UPDATE_2 = 0x01d9 +SMSG_BEING_NAME_RESPONSE = 0x0095 # Has to be requested +SMSG_BEING_ACTION = 0x008a + +CMSG_ITEM_PICKUP = 0x009f +CMSG_PLAYER_ATTACK = 0x0089 +CMSG_PLAYER_STOP_ATTACK = 0x0118 +CMSG_PLAYER_CHANGE_DIR = 0x009b +CMSG_PLAYER_CHANGE_DEST = 0x0085 +CMSG_PLAYER_EMOTE = 0x00bf +SMSG_WALK_RESPONSE = 0x0087 + +CMSG_PLAYER_INVENTORY_USE = 0x00a7 +CMSG_PLAYER_INVENTORY_DROP = 0x00a2 +CMSG_PLAYER_EQUIP = 0x00a9 +CMSG_PLAYER_UNEQUIP = 0x00ab + +CMSG_TRADE_REQUEST = 0x00e4 +CMSG_TRADE_RESPONSE = 0x00e6 +CMSG_TRADE_ITEM_ADD_REQUEST = 0x00e8 +CMSG_TRADE_CANCEL_REQUEST = 0x00ed +CMSG_TRADE_ADD_COMPLETE = 0x00eb +CMSG_TRADE_OK = 0x00ef + +SMSG_TRADE_REQUEST = 0x00e5 # Receiving a request to trade +SMSG_TRADE_RESPONSE = 0x00e7 +SMSG_TRADE_ITEM_ADD = 0x00e9 +SMSG_TRADE_ITEM_ADD_RESPONSE = 0x01b1 # Not standard eAthena! +SMSG_TRADE_OK = 0x00ec +SMSG_TRADE_CANCEL = 0x00ee +SMSG_TRADE_COMPLETE = 0x00f0 + +SMSG_ITEM_VISIBLE = 0x009d +SMSG_ITEM_DROPPED = 0x009e +SMSG_ITEM_REMOVE = 0x00a1 + +inventory_offset = 2 +storage_offset = 1 + +CMSG_SERVER_VERSION_REQUEST = 0x7530 +SMSG_SERVER_VERSION_RESPONSE = 0x7531 +CMSG_LOGIN_REGISTER = 0x0064 + +CMSG_MAP_SERVER_PING = 0x007e +CMSG_NAME_REQUEST = 0x0094 + +# party +SMSG_PARTY_CHAT = 0x0109 +CMSG_PARTY_MESSAGE = 0x0108 + +# NPC +CMSG_NPC_TALK = 0x0090 +CMSG_NPC_NEXT_REQUEST = 0x00b9 +CMSG_NPC_CLOSE = 0x0146 +CMSG_NPC_LIST_CHOICE = 0x00b8 +CMSG_NPC_INT_RESPONSE = 0x0143 +CMSG_NPC_STR_RESPONSE = 0x01d5 +CMSG_NPC_BUY_SELL_REQUEST = 0x00c5 +CMSG_NPC_BUY_REQUEST = 0x00c8 +CMSG_NPC_SELL_REQUEST = 0x00c9 + +# stats, skills +CMSG_STAT_UPDATE_REQUEST = 0x00bb +CMSG_SKILL_LEVELUP_REQUEST = 0x0112 + +# storage +CMSG_MOVE_TO_STORAGE = 0x00f3 +CMSG_MOVE_FROM_STORAGE = 0x00f5 +CMSG_CLOSE_STORAGE = 0x00f7 diff --git a/net/protocol.pyc b/net/protocol.pyc Binary files differnew file mode 100644 index 0000000..9b73f78 --- /dev/null +++ b/net/protocol.pyc diff --git a/net/stats.py b/net/stats.py new file mode 100644 index 0000000..009ac9b --- /dev/null +++ b/net/stats.py @@ -0,0 +1,44 @@ + +WALK_SPEED = 0 +EXP = 1 +JOB_EXP = 2 +KARMA = 3 +MANNER = 4 +HP = 5 +MAX_HP = 6 +MP = 7 +MAX_MP = 8 +CHAR_POINTS = 9 +LEVEL = 11 +SKILL_POINTS = 12 +STR = 13 +AGI = 14 +VIT = 15 +INT = 16 +DEX = 17 +LUK = 18 +MONEY = 20 +EXP_NEEDED = 22 +JOB_MOD = 23 +TOTAL_WEIGHT = 24 +MAX_WEIGHT = 25 +STR_NEEDED = 32 +AGI_NEEDED = 33 +VIT_NEEDED = 34 +INT_NEEDED = 35 +DEX_NEEDED = 36 +LUK_NEEDED = 37 +ATK = 41 +ATK_MOD = 42 +MATK = 43 +MATK_MOD = 44 +DEF = 45 +DEF_MOD = 46 +MDEF = 47 +MDEF_MOD = 48 +HIT = 49 +FLEE = 50 +FLEE_MOD = 51 +CRIT = 52 +ATTACK_DELAY = 53 +JOB = 55 diff --git a/net/stats.pyc b/net/stats.pyc Binary files differnew file mode 100644 index 0000000..aea5a45 --- /dev/null +++ b/net/stats.pyc diff --git a/net/trade.py b/net/trade.py new file mode 100644 index 0000000..c2982c7 --- /dev/null +++ b/net/trade.py @@ -0,0 +1,6 @@ + +def reset_trade_state(ts): + ts['zeny_give'] = 0 + ts['zeny_get'] = 0 + ts['items_give'] = [] + ts['items_get'] = [] diff --git a/net/trade.pyc b/net/trade.pyc Binary files differnew file mode 100644 index 0000000..f76375d --- /dev/null +++ b/net/trade.pyc diff --git a/newmessage.wav b/newmessage.wav Binary files differnew file mode 100644 index 0000000..9c4f550 --- /dev/null +++ b/newmessage.wav diff --git a/playerlist.py b/playerlist.py new file mode 100644 index 0000000..e8db995 --- /dev/null +++ b/playerlist.py @@ -0,0 +1,28 @@ +import os + + +class PlayerList: + def __init__(self, fn): + self._filename = fn + self._last_modified = os.path.getmtime(fn) + self._list = self._load_file(fn) + + def _load_file(self, fn): + self._list = [] + with open(fn, 'r') as f: + for l in f: + self._list.append(l.strip()) + return self._list + + def check_player(self, pn): + if pn in self._list: + return True + else: + lm = os.path.getmtime(self._filename) + if lm != self._last_modified: + self._last_modified = lm + self._load_file(self._filename) + if pn in self._list: + return True + + return False diff --git a/playerlist.pyc b/playerlist.pyc Binary files differnew file mode 100644 index 0000000..3ad58ce --- /dev/null +++ b/playerlist.pyc diff --git a/plugins/README.txt b/plugins/README.txt new file mode 100644 index 0000000..8324b3d --- /dev/null +++ b/plugins/README.txt @@ -0,0 +1,23 @@ +This directory contains plugins for ManaChat. + +To autoload the plugin, in the [Plugins] section of manachat.ini add + +[Plugins] +... +pluginname = 1 +... + +The plugin and it's dependencies will be autoloaded. +The plugin must export a variable PLUGIN, and the function init() + +PLUGIN = { + 'name' : 'PluginName' # not really used atm + 'requires' : (plugin1, plugin2, ...) # list of required plugins + 'blocks' : (plugin3, plugin4, ...) # list of incompatible plugins +} + +def init(config): # config is ConfigParser instance + # ... plugin initialisation code ... + pass + +See 'shop.py' as an example. diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..0e40be9 --- /dev/null +++ b/plugins/__init__.py @@ -0,0 +1,51 @@ +import logging +import sys + +sys.path.insert(0, "plugins") +debuglog = logging.getLogger("ManaChat.Debug") + +plugins_loaded = [] + + +class PluginError(Exception): + pass + + +def load_plugin(config, plugin_name): + if plugin_name in plugins_loaded: + return + + plugin = __import__(plugin_name) + + for p in plugin.PLUGIN['blocks']: + if p in plugins_loaded: + raise PluginError("{} blocks {}".format(p, plugin_name)) + + for p in plugin.PLUGIN['requires']: + if p not in plugins_loaded: + load_plugin(config, p) + + this = sys.modules[__name__] + setattr(this, plugin_name, plugin) + + # filling the gaps in config + if not config.has_section(plugin_name): + config.add_section(plugin_name) + + default_config = plugin.PLUGIN.setdefault('default_config', {}) + for option, value in default_config.iteritems(): + if not config.has_option(plugin_name, option): + config.set(plugin_name, option, str(value)) + + plugin.init(config) + plugins_loaded.append(plugin_name) + debuglog.info('Plugin %s loaded', plugin_name) + + +def load_plugins(config): + for pn in config.options('Plugins'): + if config.getboolean('Plugins', pn): + try: + load_plugin(config, pn) + except ImportError as e: + debuglog.error('Error loading plugin %s: %s', pn, e) diff --git a/plugins/__init__.pyc b/plugins/__init__.pyc Binary files differnew file mode 100644 index 0000000..4469184 --- /dev/null +++ b/plugins/__init__.pyc diff --git a/plugins/autofollow.py b/plugins/autofollow.py new file mode 100644 index 0000000..ff34bde --- /dev/null +++ b/plugins/autofollow.py @@ -0,0 +1,39 @@ +import net.mapserv as mapserv +import commands +from utils import extends +from loggers import debuglog + + +__all__ = [ 'PLUGIN', 'init', 'follow' ] + + +PLUGIN = { + 'name': 'autofollow', + 'requires': (), + 'blocks': (), +} + +follow = '' + + +@extends('smsg_player_move') +def player_move(data): + if follow: + b = mapserv.beings_cache[data.id] + if b.name == follow: + mapserv.cmsg_player_change_dest(data.coor_pair.dst_x, + data.coor_pair.dst_y) + + +def follow_cmd(_, player): + '''Follow given player, or disable following (if no arg)''' + global follow + follow = player + if player: + debuglog.info('Following %s', player) + else: + debuglog.info('Not following anyone') + + +def init(config): + commands.commands['follow'] = follow_cmd diff --git a/plugins/autofollow.pyc b/plugins/autofollow.pyc Binary files differnew file mode 100644 index 0000000..159fe47 --- /dev/null +++ b/plugins/autofollow.pyc diff --git a/plugins/autospell.py b/plugins/autospell.py new file mode 100644 index 0000000..30d0ec6 --- /dev/null +++ b/plugins/autospell.py @@ -0,0 +1,57 @@ +import re +import time +import commands +import net.mapserv as mapserv +from loggers import debuglog +from logicmanager import logic_manager + +__all__ = [ 'PLUGIN', 'init' ] + +PLUGIN = { + 'name': 'autospell', + 'requires': (), + 'blocks': (), + 'default_config' : {} +} + +as_re = re.compile(r'(\d+) (\d+) (.+)') +times = 0 +delay = 0 +spell = '' +next_ts = 0 + + +def cmd_autospell(_, arg): + '''Cast a spell multiple times automatically +/autospell TIMES DELAY SPELL''' + global times, delay, spell, next_ts + m = as_re.match(arg) + if m: + times = int(m.group(1)) + delay = int(m.group(2)) + spell = m.group(3) + else: + debuglog.warning("Usage: /autospell TIMES DELAY SPELL") + return + + next_ts = time.time() + delay + + +def cmd_stopspell(*unused): + '''Stop casting autospells''' + global times, delay, spell, next_ts + times = delay = next_ts = 0 + + +def autospell_logic(ts): + global times, next_ts + if times > 0 and ts >= next_ts: + times -= 1 + next_ts = ts + delay + mapserv.cmsg_chat_message(spell) + + +def init(config): + commands.commands['autospell'] = cmd_autospell + commands.commands['stopspell'] = cmd_stopspell + logic_manager.add_logic(autospell_logic) diff --git a/plugins/autospell.pyc b/plugins/autospell.pyc Binary files differnew file mode 100644 index 0000000..362aab6 --- /dev/null +++ b/plugins/autospell.pyc diff --git a/plugins/battlebot.py b/plugins/battlebot.py new file mode 100644 index 0000000..5cba467 --- /dev/null +++ b/plugins/battlebot.py @@ -0,0 +1,227 @@ +import time +import net.mapserv as mapserv +import net.charserv as charserv +import net.stats as stats +import walkto +import logicmanager +import commands +from net.common import distance +from net.inventory import get_item_index +from utils import extends +from loggers import debuglog +from actor import find_nearest_being + + +__all__ = [ 'PLUGIN', 'init', + 'hp_healing_ids', 'hp_heal_at', 'mp_healing_ids', 'mp_heal_at', + 'auto_attack', 'auto_pickup', 'auto_heal_self', + 'auto_heal_others' ] + + +PLUGIN = { + 'name': 'battlebot', + 'requires': (), + 'blocks': (), +} + +target = None +# last_time_attacked = 0 +aa_next_time = 0 + +hp_healing_ids = [ 535, 541 ] +hp_heal_at = 0.3 +hp_is_healing = False +hp_prev_value = 0 + +mp_healing_ids = [ 826 ] +mp_heal_at = 0.5 +mp_is_healing = False +mp_prev_value = 0 + +players_taken_damage = {} +player_damage_heal = 300 + +aa_monster_types = [] + +auto_pickup = True +auto_attack = False +auto_heal_self = False +auto_heal_others = False + + +@extends('smsg_being_action') +def being_action(data): + # global last_time_attacked + global aa_next_time + + if data.type in (0, 10): + + if data.src_id == charserv.server.account: + # last_time_attacked = time.time() + aa_next_time = time.time() + 5.0 + + if (auto_heal_others and + data.dst_id != charserv.server.account and + data.dst_id in mapserv.beings_cache and + mapserv.beings_cache[data.dst_id].type == 'player'): + + players_taken_damage[data.dst_id] = players_taken_damage.get( + data.dst_id, 0) + data.damage + + if players_taken_damage[data.dst_id] >= player_damage_heal: + mapserv.cmsg_chat_message("#inma {}".format( + mapserv.beings_cache[data.dst_id].name)) + players_taken_damage[data.dst_id] = 0 + + +@extends('smsg_item_dropped') +@extends('smsg_item_visible') +def flooritem_appears(data): + if not auto_pickup: + return + + item = mapserv.floor_items[data.id] + px = mapserv.player_pos['x'] + py = mapserv.player_pos['y'] + + if distance(px, py, item.x, item.y) > 3: + return + + walkto.walkto_and_action(item, 'pickup') + + +@extends('smsg_player_status_change') +def player_status_change(data): + global hp_is_healing + if data.id == charserv.server.account: + if data.effect == 256: + hp_is_healing = True + elif data.effect == 0: + hp_is_healing = False + + +@extends('smsg_player_stat_update_x') +def player_stat_update(data): + if not auto_heal_self: + return + + global hp_prev_value, mp_prev_value + + if data.type == stats.HP: + max_hp = mapserv.player_stats.get(stats.MAX_HP, 0) + if data.stat_value < max_hp * hp_heal_at and not hp_is_healing: + healing_found = False + for item_id in hp_healing_ids: + index = get_item_index(item_id) + if index > 0: + healing_found = True + debuglog.info("Consuming %d", item_id) + mapserv.cmsg_player_inventory_use(index, item_id) + break + if not healing_found: + debuglog.info("Low health, but no HP healing item found") + + hp_prev_value = data.stat_value + + elif data.type == stats.MP: + max_mp = mapserv.player_stats.get(stats.MAX_MP, 0) + if data.stat_value < max_mp * mp_heal_at and not mp_is_healing: + healing_found = False + for item_id in mp_healing_ids: + index = get_item_index(item_id) + if index > 0: + healing_found = True + debuglog.info("Consuming %d", item_id) + mapserv.cmsg_player_inventory_use(index, item_id) + break + + if not healing_found: + debuglog.info("Low mana, but no MP healing item found") + + mp_prev_value = data.stat_value + + +@extends('smsg_being_remove') +def being_remove(data): + global target + if target is not None and target.id == data.id: + target = None + aa_next_time = time.time() + 5.0 + + +def battlebot_logic(ts): + + if not auto_attack: + return + + global target + # global last_time_attacked + global aa_next_time + + if ts < aa_next_time: + return + + if target is None: + if walkto.state: + return + + target = find_nearest_being(type='monster', + ignored_ids=walkto.unreachable_ids, + allowed_jobs=aa_monster_types) + if target is not None: + # last_time_attacked = time.time() + aa_next_time = time.time() + 5.0 + walkto.walkto_and_action(target, 'attack') + + elif ts > aa_next_time: + walkto.walkto_and_action(target, 'attack') + + +def startbot(_, arg): + '''Start autoattacking and autolooting''' + global auto_attack + global auto_pickup + global aa_monster_types + auto_attack = True + auto_pickup = True + try: + aa_monster_types = map(int, arg.split()) + except ValueError: + aa_monster_types = [] + + +def stopbot(cmd, _): + '''Stop battlebot''' + global auto_attack + global auto_pickup + global auto_heal_self + global auto_heal_others + global target + auto_attack = False + auto_pickup = False + auto_heal_self = False + auto_heal_others = False + if target is not None: + mapserv.cmsg_player_stop_attack() + target = None + + +def debugbot(cmd, _): + px = mapserv.player_pos['x'] + py = mapserv.player_pos['y'] + target_info = '<no_target>' + if target is not None: + target_info = '{} at ({},{})'.format(target.name, target.x, target.y) + debuglog.info('target = %s | player at (%d, %d)', target_info, px, py) + + +bot_commands = { + 'startbot' : startbot, + 'stopbot' : stopbot, + 'debugbot' : debugbot, +} + + +def init(config): + logicmanager.logic_manager.add_logic(battlebot_logic) + commands.commands.update(bot_commands) diff --git a/plugins/battlebot.pyc b/plugins/battlebot.pyc Binary files differnew file mode 100644 index 0000000..c48b64d --- /dev/null +++ b/plugins/battlebot.pyc diff --git a/plugins/chatbot.py b/plugins/chatbot.py new file mode 100644 index 0000000..e26928d --- /dev/null +++ b/plugins/chatbot.py @@ -0,0 +1,71 @@ +import re +import random +import types +import net.mapserv as mapserv +from utils import extends +import chat + + +__all__ = [ 'PLUGIN', 'init', 'answer', 'add_command', 'remove_command' ] + + +PLUGIN = { + 'name': 'chatbot', + 'requires': (), + 'blocks': (), +} + +commands = {} + + +def answer_info(nick, message, is_whisper, match): + if is_whisper: + chat.send_whisper(nick, "This is an experimental bot.") + +def answer_random(nick, message, is_whisper, answers): + resp = random.choice(answers) + if is_whisper: + chat.send_whisper(nick, resp) + else: + mapserv.cmsg_chat_message(resp) + +def answer(nick, message, is_whisper): + try: + for regex, action in commands.iteritems(): + match = regex.match(message) + if match: + if isinstance(action, types.ListType): + answer_random(nick, message, is_whisper, action) + elif isinstance(action, types.FunctionType): + action(nick, message, is_whisper, match) + else: + raise ValueError("must be either list or function") + except: + answer_random(nick, message, is_whisper, action) + +@extends('smsg_being_chat') +def being_chat(data): + idx = data.message.find(' : ') + if idx > -1: + nick = data.message[:idx] + message = data.message[idx + 3:] + answer(nick, message, False) + + +@extends('smsg_whisper') +def got_whisper(data): + nick, message = data.nick, data.message + answer(nick, message, True) + + +def add_command(cmd, action): + cmd_re = re.compile(cmd) + commands[cmd_re] = action + +def remove_command(cmd): + cmd_re = re.compile(cmd) + commands.remove(cmd_re) + +def init(config): + add_command('!info', answer_info) + add_command('!random', ['asd', 'Ciao!']) diff --git a/plugins/chatbot.pyc b/plugins/chatbot.pyc Binary files differnew file mode 100644 index 0000000..b327a9e --- /dev/null +++ b/plugins/chatbot.pyc diff --git a/plugins/chatlogfile.py b/plugins/chatlogfile.py new file mode 100644 index 0000000..7d97c8c --- /dev/null +++ b/plugins/chatlogfile.py @@ -0,0 +1,109 @@ +import os +import logging + +import net.mapserv as mapserv +from loggers import chatlog +from utils import extends + + +__all__ = [ 'PLUGIN', 'init', 'ChatLogHandler' ] + + +PLUGIN = { + 'name': 'chatlogfile', + 'requires': (), + 'blocks': (), + 'default_config' : {'chatlog_dir': 'chatlogs'} +} + + +class ChatLogHandler(logging.Handler): + + def __init__(self, chat_log_dir): + logging.Handler.__init__(self, 0) + self.chat_log_dir = chat_log_dir + self.loggers = {} + if not os.path.exists(self.chat_log_dir): + os.makedirs(self.chat_log_dir) + self._count = 0 + + def emit(self, record): + try: + user = record.user + except AttributeError: + return + + user = ''.join(map(lambda c: c if c.isalnum() else '_', user)) + + if user in self.loggers: + logger = self.loggers[user] + else: + logger = chatlog.getChild(user) + self.loggers[user] = logger + handler = logging.FileHandler(os.path.join( + self.chat_log_dir, user + ".txt")) + logger.addHandler(handler) + + self._count += 1 + logger.count = self._count + + if len(self.loggers) > 5: + min_count = self._count + old_user = '' + for usr, lgr in self.loggers.items(): + if lgr.count < min_count: + old_user = user + min_count = lgr.count + self.loggers[old_user].handlers[0].close() + del self.loggers[old_user] + + message = self.format(record) + logger.info(message) + + +def log(message, user='General'): + chatlog.info(message, extra={'user': user}) + + +@extends('smsg_being_chat') +def being_chat(data): + log(data.message) + + +@extends('smsg_player_chat') +def player_chat(data): + message = data.message + log(message) + + +@extends('smsg_whisper') +def got_whisper(data): + nick, message = data.nick, data.message + m = "[{} ->] {}".format(nick, message) + log(m, nick) + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + if data.code == 0: + m = "[-> {}] {}".format(mapserv.last_whisper['to'], + mapserv.last_whisper['msg']) + log(m, mapserv.last_whisper['to']) + + +@extends('smsg_party_chat') +def party_chat(data): + nick = mapserv.party_members.get(data.id, str(data.id)) + msg = data.message + m = "[Party] {} : {}".format(nick, msg) + log(m, "Party") + + +def init(config): + chatlog_dir = config.get('chatlogfile', 'chatlog_dir') + + clh = ChatLogHandler(chatlog_dir) + clh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S")) + chatlog.addHandler(clh) + chatlog.setLevel(logging.INFO) diff --git a/plugins/chatlogfile.pyc b/plugins/chatlogfile.pyc Binary files differnew file mode 100644 index 0000000..c6fad9b --- /dev/null +++ b/plugins/chatlogfile.pyc diff --git a/plugins/guildbot/__init__.py b/plugins/guildbot/__init__.py new file mode 100644 index 0000000..0e4ffd5 --- /dev/null +++ b/plugins/guildbot/__init__.py @@ -0,0 +1,18 @@ +import handlers +from guilddb import GuildDB +from net.onlineusers import OnlineUsers + +PLUGIN = { + 'name': 'guildbot', + 'requires': (), + 'blocks': (), +} + +__all__ = ['PLUGIN', 'init'] + + +def init(config): + handlers.online_users = OnlineUsers(config.get('Other', 'online_txt_url'), + refresh_hook=handlers.online_list_update) + handlers.online_users.start() + handlers.db = GuildDB(config.get('GuildBot', 'dbfile')) diff --git a/plugins/guildbot/__init__.pyc b/plugins/guildbot/__init__.pyc Binary files differnew file mode 100644 index 0000000..f10c5b9 --- /dev/null +++ b/plugins/guildbot/__init__.pyc diff --git a/plugins/guildbot/create_db.sql b/plugins/guildbot/create_db.sql new file mode 100644 index 0000000..bdf1d76 --- /dev/null +++ b/plugins/guildbot/create_db.sql @@ -0,0 +1,23 @@ +pragma foreign_keys=on; + +create table if not exists GUILDS( + ID integer primary key not null, + NAME text[25] not null unique, + CREATED datetime default current_timestamp, + MOTD text[100]); + +create table if not exists PLAYERS( + ID integer primary key not null, + NAME text[25] not null unique, + LASTSEEN datetime, + GUILD_ID integer, + ACCESS integer default 0, + foreign key(GUILD_ID) references GUILDS(ID)); + +insert into GUILDS(NAME) values('Crew of Red Corsair'); +insert into GUILDS(NAME) values('Phoenix Council'); + +insert into PLAYERS(NAME, GUILD_ID, ACCESS) values('Travolta', 1, 10); +insert into PLAYERS(NAME, GUILD_ID, ACCESS) values('Rill', 1, 10); +insert into PLAYERS(NAME, GUILD_ID, ACCESS) values('veryape', 2, 10); + diff --git a/plugins/guildbot/guilddb.py b/plugins/guildbot/guilddb.py new file mode 100644 index 0000000..a720b49 --- /dev/null +++ b/plugins/guildbot/guilddb.py @@ -0,0 +1,153 @@ +import logging +import sys +import sqlite3 + + +class GuildDB: + + def __init__(self, dbfile): + self._dbfile = dbfile + self.logger = logging.getLogger('ManaChat.Guild') + self.db, self.cur = self._open_sqlite_db(dbfile) + + # self.cur.execute('PRAGMA foreign_keys = ON') + self.cur.execute('create table if not exists GUILDS(\ + ID integer primary key,\ + NAME text[25] not null unique,\ + CREATED datetime default current_timestamp,\ + MOTD text[100])') + self.cur.execute('create table if not exists PLAYERS(\ + ID integer primary key,\ + NAME text[25] not null unique,\ + LASTSEEN date,\ + GUILD_ID integer,\ + ACCESS integer not null default -10,\ + SHOWINFO boolean not null default 0,\ + foreign key(GUILD_ID) references GUILDS(ID))') + self.db.commit() + + def __del__(self): + try: + self.db.close() + except Exception: + pass + + def _open_sqlite_db(self, dbfile): + """ + Open sqlite db, and return tuple (connection, cursor) + """ + try: + db = sqlite3.connect(dbfile) + cur = db.cursor() + except sqlite3.Error, e: + self.logger.error("sqlite3 error: %s", e.message) + sys.exit(1) + return db, cur + + def guild_create(self, name): + self.cur.execute('insert into GUILDS(NAME) values(?)', (name,)) + self.db.commit() + if self.cur.rowcount: + self.logger.info('Created guild "%s"', name) + return True + else: + self.logger.info('Error creating guild "%s"', name) + return False + + def guild_delete(self, name): + self.cur.execute('select ID from GUILDS where name = ?', (name,)) + row = self.cur.fetchone() + if row: + guild_id = row[0] + self.cur.execute('delete from GUILDS where name=?', (name,)) + self.cur.execute('update PLAYERS set GUILD_ID = NULL, \ + ACCESS = -10, where GUILD_ID = ?', (guild_id,)) + self.db.commit() + self.logger.info('Deleted guild "%s"', name) + return True + else: + self.logger.error('Guild "%s" not found', name) + return False + + def guild_set_motd(self, name, motd): + self.cur.execute('update GUILD set MOTD = ? where NAME = ?', + (motd, name)) + self.db.commit() + if self.cur.rowcount: + self.logger.info('Guild "%s" MOTD: %s', name, motd) + return True + else: + self.logger.error('Error setting MOTD for guild: %s', name) + return False + + def player_info(self, name): + query = '''select GUILDS.ID,GUILDS.NAME,ACCESS + from PLAYERS join GUILDS + on PLAYERS.GUILD_ID = GUILDS.ID + where PLAYERS.NAME = ?''' + self.cur.execute(query, (name,)) + return self.cur.fetchone() + + def player_get_access(self, name, guild_name=''): + query = 'select ACCESS from PLAYERS where NAME = ?' + self.cur.execute(query, (name, guild_name)) + row = self.cur.fetchone() + if row: + return row[0] + else: + # self.logger.warning('player %s not found', name) + return -10 + + def player_set_access(self, name, access_level): + query = '''update table PLAYERS set ACCESS = ? + where name = ?''' + self.cur.execute(query, (name, access_level)) + self.db.commit() + + def player_join_guild(self, player, guild, access=0): + self.cur.execute('select ID from GUILDS where NAME = ?', (guild,)) + guild_info = self.cur.fetchone() + if guild_info: + guild_id = guild_info[0] + else: + self.logger.error('Guild "%s" not found', guild) + return False + + query = '''update or ignore PLAYERS + set GUILD_ID = ?, ACCESS = ? + where NAME = ?''' + self.cur.execute(query, (guild_id, access)) + + query = '''insert or ignore into + PLAYERS(NAME, GUILD_ID, ACCESS) + values(?, ?, ?)''' + self.cur.execute(query, (player, guild_id, access)) + + self.db.commit() + + self.logger.info('Added player "%s" to guild "%s"', + player, guild) + return True + + def player_set_showinfo(self, player, si=True): + query = '''update table PLAYERS set SHOWINFO = ? + where name = ?''' + self.cur.execute(query, (player, si)) + self.db.commit() + + def guild_remove_player(self, player_name): + query = '''update PLAYERS set GUILD_ID = NULL, ACCESS = -10 + where NAME = ?''' + self.cur.execute(query, (player_name,)) + self.db.commit() + + def all_players_same_guild(self, player_name): + query = '''select NAME from PLAYERS + where GUILD_ID = (select GUILD_ID from PLAYERS + where NAME = ?)''' + return self.cur.fetchall(query, (player_name,)) + + def all_players_any_guild(self): + query = '''select NAME from PLAYERS + where ACCESS >= 0''' + return self.cur.fetchall(query) diff --git a/plugins/guildbot/guilddb.pyc b/plugins/guildbot/guilddb.pyc Binary files differnew file mode 100644 index 0000000..9de8f66 --- /dev/null +++ b/plugins/guildbot/guilddb.pyc diff --git a/plugins/guildbot/handlers.py b/plugins/guildbot/handlers.py new file mode 100644 index 0000000..6d032b5 --- /dev/null +++ b/plugins/guildbot/handlers.py @@ -0,0 +1,326 @@ +from chat import send_whisper +from utils import extends +from commands import parse_player_name + + +online_users = None +db = None +max_msg_len = 200 +pending_invitations = {} + + +def ignore(): + pass + + +def listonline(nick, _): + curr_msg = '' + online = online_users.online_users + + for prow in db.all_players_same_guild(nick): + p = prow[0] + if p in online: + if len(curr_msg + ', ' + p) > max_msg_len: + send_whisper(nick, curr_msg) + curr_msg = p + else: + curr_msg = curr_msg + ', ' + p + + send_whisper(nick, curr_msg) + + +def leave(nick, _): + info = db.player_info(nick) + broadcast(nick, '"{}" left the guild'.format(nick), True) + db.guild_remove_player(nick) + send_whisper(nick, 'You left guild {}'.format(info[1])) + + +def showinfo(nick, _): + db.player_set_showinfo(nick, True) + send_whisper(nick, "Information messages are visible") + + +def hideinfo(nick, _): + db.player_set_showinfo(nick, False) + send_whisper(nick, "Information messages are hidden") + + +def status(nick, _): + _, guild, access = db.player_info(nick) + send_whisper(nick, 'Player:{}, Guild:{}, Access:{}'.format( + nick, guild, access)) + + +# FIXME: not finished +def invite(nick, player): + if not player: + send_whisper(nick, "Usage: !invite Player") + return + + pinfo = db.player_info(player) + if pinfo and pinfo[0]: + send_whisper(nick, '"{}" is already a member of guild "{}"'.format( + player, pinfo[1])) + return + + online = online_users.online_users + if player not in online: + send_whisper(nick, '"{}" is not online'.format(player)) + return + + _, guild, _ = db.player_info(nick) + invite_msg = ('You have been invited to the "{}" guild chat. ' + 'If you would like to accept this invitation ' + 'please reply "yes" and if not then "no"').format(guild) + send_whisper(player, invite_msg) + # FIXME: what if player is offline? online_list can be outdated + pending_invitations[player] = guild + + +def remove(nick, player): + if not player: + send_whisper(nick, "Usage: !remove Player") + return + + pinfo = db.player_info(player) + if not pinfo: + send_whisper(nick, '{} is not in any guild'.format(player)) + return + + gid, _, _ = db.player_info(nick) + if gid != pinfo[0]: + send_whisper(nick, '{} is not in your guild'.format(player)) + return + + broadcast(player, '{} was removed from your guild'.format(player), True) + db.guild_remove_player(player) + send_whisper(nick, 'You were removed from "{}" guild'.format(pinfo[1])) + + +def setmotd(nick, motd): + guild = db.player_info(nick)[1] + db.setmotd(guild, motd) + broadcast(nick, 'MOTD: ' + motd) + + +def removemotd(nick, _): + guild = db.player_info(nick)[1] + db.setmotd(guild, '') + broadcast(nick, 'MOTD removed') + + +def setaccess(nick, params): + try: + si = params.index(" ") + lvl = int(params[:si]) + player = params[si + 1:] + if len(player) < 4: + raise ValueError + except ValueError: + send_whisper(nick, "Usage: !setaccess Level Player") + return + + gid, guild_name, access = db.player_info(nick) + gidp, _, accessp = db.player_info(player) + + if gid != gidp: + send_whisper(nick, '{} is not in your guild "{}"'.format( + player, guild_name)) + return + + if access <= accessp: + send_whisper(nick, "You cannot set access level for {}".format( + player)) + return + + db.player_set_access(player, lvl) + send_whisper(nick, "Player: {}, access level: {}".format( + player, lvl)) + + +def disband(nick, _): + _, guild, _ = db.player_info(nick) + if db.guild_delete(guild): + send_whisper(nick, 'Deleted guild "{}"'.format(guild)) + else: + send_whisper(nick, 'Error deleting guild "{}"'.format(guild)) + + +def addguild(nick, params): + usage = 'Usage: !addguild Leader Guild (note: Leader can be quoted)' + if not params: + send_whisper(nick, usage) + return + + leader, guild = parse_player_name(params) + + if len(leader) < 4 or len(guild) < 4: + send_whisper(nick, usage) + return + + if db.guild_create(guild): + send_whisper(nick, 'Created guild "{}", leader is "{}"'.format( + guild, leader)) + else: + send_whisper(nick, "Error creating guild") + + +def removeguild(nick, guild_name): + if not guild_name: + send_whisper(nick, "Usage: !removeguild Guild") + return + + if db.guild_delete(guild_name): + send_whisper(nick, 'Deleted guild "{}"'.format(guild_name)) + else: + send_whisper(nick, 'Guild not found: "{}"'.format(guild_name)) + + +def globalmsg(nick, msg): + if not msg: + send_whisper(nick, "Usage: !global Message") + return + + online = online_users.online_users + for prow in db.all_players_any_guild(): + pname = prow[0] + if pname in online: + send_whisper(pname, msg) + + +def joinguild(nick, guild): + if not guild: + send_whisper(nick, "Usage: !joinguild Guild") + return + + if db.player_join_guild(nick, guild, 20): + send_whisper(nick, 'You joined guild "{}"'.format(guild)) + else: + send_whisper(nick, 'Guild "{}" does not exist'.format(guild)) + + +def showhelp(nick, _): + access = db.player_get_access(nick) + curr_line = '' + + for cmd, (lvl, _, hlp) in commands.iteritems(): + if access < lvl: + continue + + if hlp[0] == '+': + help_s = '!' + cmd + ' ' + hlp[1:] + else: + help_s = '!' + cmd + ' -- ' + hlp + + if len(curr_line + '; ' + help_s) > max_msg_len: + send_whisper(nick, curr_line) + curr_line = help_s + else: + curr_line = curr_line + '; ' + help_s + + if curr_line: + send_whisper(nick, curr_line) + + +commands = { + "help": (-10, showhelp, "show help"), + "info": (0, status, "display guild information"), + "listonline": (0, listonline, "list online players"), + "leave": (0, leave, "leave your guild"), + "showinfo": (0, showinfo, "verbose notifications"), + "hideinfo": (0, hideinfo, "quiet notifications"), + "invite": (5, invite, "+Player -- invite player to guild"), + "remove": (5, remove, "+Player -- remove player from guild"), + "setmotd": (5, setmotd, "+MOTD -- set MOTD"), + "removemotd": (5, removemotd, "remove MOTD"), + "setaccess": (10, setaccess, "+Level Player -- set access level"), + "disband": (10, disband, "disband your guild"), + "addguild": (20, addguild, "+Leader GuildName -- add guild"), + "removeguild": (20, removeguild, "+GuildName -- remove guild"), + "global": (20, globalmsg, "+Message -- global message"), + "joinguild": (20, joinguild, "+GuildName -- join a guild"), +} + + +def exec_command(nick, cmdline): + end = cmdline.find(" ") + if end < 0: + cmd = cmdline[1:] + arg = "" + else: + cmd = cmdline[1:end] + arg = cmdline[end + 1:] + + if cmd in commands: + lvl, func, _ = commands[cmd] + access = db.player_get_access(nick) + + if access < lvl: + send_whisper(nick, 'That command is fobidden for you!') + else: + func(nick, arg) + + else: + send_whisper(nick, 'Command !{} not found. Try !help'.format(cmd)) + + +def player_joining(player, guild): + db.player_join_guild(player, guild) + broadcast(player, '{} joined your guild'.format(player), True) + + +def broadcast(nick, msg, exclude_nick=False): + """ + Broadcast message for all players that belong the same guild as nick. + """ + n = 0 + for prec in db.all_players_same_guild(nick): + if exclude_nick and prec[0] == nick: + continue + n += 1 + send_whisper(prec[0], '{} : {}'.format(nick, msg)) + + if n == 0: + send_whisper(nick, "You don't belong to any guild") + + +def online_list_update(curr, prev): + for p in curr - prev: + ginfo = db.player_info(p) + if ginfo is not None: + if ginfo[0] is not None: + allp = set(db.all_players_same_guild(p)) + n = len(allp.intersection(curr)) + send_whisper(p, + 'Welcome to {}! ({} Members are online)'.format( + ginfo[1], n)) + broadcast(p, '{} is now Online'.format(p), True) + + for p in prev - curr: + broadcast(p, '{} is now Offline'.format(p), True) + + +@extends('smsg_whisper') +def got_whisper(data): + nick, message = data.nick, data.message + + if len(message) < 1: + return + + if message[0] == '!': + exec_command(nick, message) + else: + if nick in pending_invitations: + # TODO: inform message + if message.lower() == 'yes': + player_joining(nick, pending_invitations[nick]) + del pending_invitations[nick] + + else: + broadcast(nick, message) + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + pass diff --git a/plugins/guildbot/handlers.pyc b/plugins/guildbot/handlers.pyc Binary files differnew file mode 100644 index 0000000..a109f0c --- /dev/null +++ b/plugins/guildbot/handlers.pyc diff --git a/plugins/lazytree.py b/plugins/lazytree.py new file mode 100644 index 0000000..64efda0 --- /dev/null +++ b/plugins/lazytree.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +import random +import chatbot +from commands import general_chat, send_whisper + + +__all__ = [ 'PLUGIN', 'init' ] + +PLUGIN = { + 'name': 'lazytree', + 'requires': ('chatbot',), + 'blocks': (), +} + + +# ----------------------------------------------------------------------------- +greetings = { + "Hi {0}!" : 4, + "Hey {0}" : 3, + "Yo {0}" : 2, + "{0}!!!!" : 1, + "{0}!!!" : 1, + "{0}!!" : 1, + "Hello {0}" : 5, + "Hello {0}!" : 5, + "Welcome back {0}" : 3, + "Hello {0}! You are looking lovely today!" : 1, + "{0} is back!!" : 1, + "Hello and welcome to the Aperture Science \ +computer-aided enrichment center." : 1, +} + +drop_items = [ + "a bomb", "a bowl of petunias", "a cake", "a candy", "a chocobo", + "a coin", "a cookie", "a drunken pirate", "a freight train", + "a fruit", "a mouboo", "an angry cat", + "an angry polish spelling of a rare element with the atomic number 78", + "an anvil", "an apple", "an iten", "a magic eightball", "a GM", + "a whale", "an elephant", "a piano", "a piece of moon rock", "a pin", + "a rock", "a tub", "a wet mop", "some bass", "Voldemort", "a sandworm", + "a princess", "a prince", "an idea", "Luvia", "a penguin", + "The Hitchhiker's Guide to the Galaxy", +] + +dropping_other = [ + "Hu hu hu.. {0} kicked me!", + "Ouch..", + "Ouchy..", + "*drops dead*", + "*sighs*", + "Leaf me alone.", + "Stop it! I doesn't drop branches, try the Druid tree for once!", +] + +dropping_special = { + "ShaiN2" : "*drops a nurse on {0}*", + "Shainen" : "*drops a nurse on {0}*", + "Silent Dawn" : "*drops a box of chocolate on {0}*", + "veryape" : "*drops a chest of rares on {0}*", + "veryapeGM" : "*drops a chest of rares on {0}*", + "Ginaria" : "*drops a bluepar on {0}*", + "Rift Avis" : "*drops an acorn on {0}*", +} + +die_answers = [ + "*drops a bomb on {0}'s head*", + "*drops a bowl of petunias on {0}'s head*", + "*drops a drunken pirate on {0}'s head*", + "*drops a freight train on {0}'s head*", + "*drops a mouboo on {0}'s head*", + "*drops an angry cat on {0}'s head*", + "*drops an angry polish spelling of a rare element with \ +the atomic number 78 on {0}'s head*", + "*drops an iten on {0}'s head*", + "*drops a piano on {0}'s head*", + "*drops a piece of moon rock on {0}'s head*", + "*drops Voldemort on {0}'s head*", + "*drops dead*", + "*sighs*", + "Avada Kedavra!", + "Make me!", + "Never!!", + "You die, {0}!", + "You die, {0}!", + "You die, {0}!", + "You die, {0}!", + "No!", + "In a minute..", + "Suuure... I'll get right on it", +] + +healme_answers = [ + "Eat an apple, they're good for you.", + "If I do it for you, then I have to do it for everybody.", + "Oh, go drink a potion or something.", + "Whoops! I lost my spellbook.", + "no mana", +] + +whoami_answers = [ + "An undercover GM.", + "An exiled GM.", + "I'm not telling you!", + "I'm a bot! I'll be level 99 one day! Mwahahahaaha!!!111!", + "Somebody said I'm a Chinese copy of Confused Tree", + "I am your evil twin.", + "I don't remember anything after I woke up! What happened to me?", + "I don't know. Why am I here??", + "Who are you?", + "On the 8th day, God was bored and said 'There will be bots'. \ +So here I am.", + "♪ I'm your hell, I'm your dream, I'm nothing in between ♪♪", + "♪♪ Aperture Science. We do what we must, because.. we can ♪", + "I'm just a reincarnation of a copy.", +] + +joke_answers = [ + "How did the tree get drunk? On root beer.", + "Do you think I'm lazy?", + "I miss Confused Tree :(", + "I miss CrazyTree :(", + "I'm not telling you!", + "*sighs*", + "If I do it for you, then I have to do it for everybody.", + "What did the beaver say to the tree? It's been nice gnawing you.", + "What did the little tree say to the big tree? Leaf me alone.", + "What did the tree wear to the pool party? Swimming trunks.", + "What do trees give to their dogs? Treets.", + "What do you call a tree that only eats meat? Carniforous.", + "What do you call a tree who's always envious? Evergreen.", + "What is the tree's least favourite month? Sep-timber!", + "What kind of tree can fit into your hand? A palm-tree.", + "What was the tree's favorite subject in school? Chemistree.", + "Why did the leaf go to the doctor? It was feeling green.", + "Why doesn't the tree need sudo? Because it has root.", + "Why was the cat afraid of the tree? Because of its bark.", + "Why was the tree executed? For treeson.", + "How do trees get on the internet? They log in.", + "Why did the pine tree get into trouble? Because it was being knotty.", + "Did you hear the one about the oak tree? It's a corn-y one!", + "What do you call a blonde in a tree with a briefcase? Branch Manager.", + "How is an apple like a lawyer? They both look good hanging from a tree.", + "Why did the sheriff arrest the tree? Because its leaves rustled.", + "I'm to tired, ask someone else.", + "If you are trying to get me to tell jokes you are barking \ +up the wrong tree!", + "You wodden think they were funny anyhow. Leaf me alone!", + "What is brown and sticky? A stick.", +] + +burn_answers = [ + "*curses {0} and dies %%c*", + "Help! I'm on fire!", + "Oh hot.. hot hot!", + "*is glowing*", + "*is flaming*", + "ehemm. where are firefighters? I need them now!", + "*is so hot!*", +] + +noidea_answers = [ + "what?", "what??", "whatever", "hmm...", "huh?", "*yawns*", + "Wait a minute..", "What are you talking about?", + "Who are you?", "What about me?", + "I don't know what you are talking about", + "Excuse me?", "very interesting", "really?", + "go on...", "*scratches its leafy head*", + "*feels a disturbance in the force*", + "*senses a disturbance in the force*", + "*humming*", "I'm bored..", "%%j", "%%U", "%%[", +] + +pain_answers = [ "Ouch..", "Ouchy..", "Argh..", "Eckk...", "*howls*", + "*screams*", "*groans*", "*cries*", "*faints*", "%%k", + "Why.. What did I do to you? %%i" ] + +hurt_actions = [ "eat", "shoot", "pluck", "torture", "slap", "poison", + "break", "stab", "throw" ] + +ignored_players = [] +tree_admins = [ 'Livio' ] + + +# ----------------------------------------------------------------------------- +def say_greeting(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + total_weight = 0 + for w in greetings.itervalues(): + total_weight += w + + random_weight = random.randint(0, total_weight) + total_weight = 0 + random_greeting = 'Hi {0}' + for g, w in greetings.iteritems(): + if total_weight >= random_weight: + random_greeting = g + break + total_weight += w + + general_chat(random_greeting.format(nick)) + + +def drop_on_head(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + answer = 'yeah' + if nick in dropping_special: + answer = dropping_special[nick] + else: + r = random.randint(0, len(drop_items) + len(dropping_other)) + if r < len(drop_items): + answer = "*drops {} on {}'s head*".format(drop_items[r], nick) + else: + answer = random.choice(dropping_other) + + general_chat(answer.format(nick)) + + +def answer_threat(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + answer = random.choice(die_answers) + general_chat(answer.format(nick)) + + +# ----------------------------------------------------------------------------- +def admin_additem(nick, _, is_whisper, match): + if not is_whisper: + return + + if nick not in tree_admins: + return + + item = match.group(1) + if item not in drop_items: + drop_items.append(item) + + send_whisper(nick, "Added item '{}' to drop list".format(item)) + + +def admin_addjoke(nick, _, is_whisper, match): + if not is_whisper: + return + + if nick not in tree_admins: + return + + joke = match.group(1) + if joke not in joke_answers: + joke_answers.append(joke) + + send_whisper(nick, "Added joke") + + +# ----------------------------------------------------------------------------- +tree_commands = { + r'^(hello|hi|hey|heya|hiya|yo) (livio|liviobot)' : say_greeting, + r'^(hello|hi|hey|heya|hiya) (all|everybody|everyone)$' : say_greeting, + r'\*?((shake|kick)s?) (livio|liviobot)' : drop_on_head, + r'(die|\*?((nuke|kill)s?)) (livio|liviobot)' : answer_threat, + r'^tell (.*)joke([ ,]{1,2})tree' : joke_answers, + r'^heal me([ ,]{1,2})tree' : healme_answers, + r'^(who|what) are you([ ,]{1,3})tree' : whoami_answers, + r'^!additem (.*)' : admin_additem, + r'^!addjoke (.*)' : admin_addjoke, + r'\*(burn(s?)) (livio|liviobot)' : burn_answers, + r'\*?(' + '|'.join(hurt_actions) + ')s? (livio|liviobot)' : pain_answers, +} + + +def init(config): + chatbot.commands.update(tree_commands) diff --git a/plugins/lazytree.pyc b/plugins/lazytree.pyc Binary files differnew file mode 100644 index 0000000..b7bb800 --- /dev/null +++ b/plugins/lazytree.pyc diff --git a/plugins/manaboy.py b/plugins/manaboy.py new file mode 100644 index 0000000..127ddd0 --- /dev/null +++ b/plugins/manaboy.py @@ -0,0 +1,1390 @@ +# -*- coding: utf-8 -*- +import time +import net.mapserv as mapserv +import net.charserv as charserv +import net.stats as stats +import commands +import walkto +import logicmanager +import status +import plugins +import itemdb +import random +from collections import deque +from net.inventory import get_item_index, get_storage_index +from utils import extends +from actor import find_nearest_being +from chat import send_whisper as whisper + +from net.onlineusers import OnlineUsers + +__all__ = [ 'PLUGIN', 'init' ] + +def preloadArray(nfile): + try: + file = open(nfile, "r") + array=[] + for x in file.readlines(): + x = x.replace("\n", "") + x = x.replace("\r", "") + array.append(x) + file.close() + return array + except: + print "preloadArray: File " + nfile + " not found!" + +joke_answers = preloadArray("bot/jokes.txt") +ignored_players = preloadArray("bot/ignored.txt") +disliked_players = preloadArray("bot/disliked.txt") +admins = preloadArray("bot/admins.txt") +friends = preloadArray("bot/friends.txt") + +# ====================== XCOM ============= +XCOMList = preloadArray("bot/XCOM.txt") +XCOMServerStatInterested = [] #List of nicks interested in server status change +XCOMBroadcastPrefix = "##B##G " + + +def online_list_update(curr,prev): + for x in curr: + found = False + for y in prev: + if x==y: found = True + if found == False: #detected change + for nicks in XCOMList: #For every XCOM user... + if nicks in online_users.online_users: #That's online... + if nicks in XCOMServerStatInterested: #If XCOM player is interested + if x in XCOMList: #An XCOM user connected? + XCOMDelay() #Share its status + whisper(nicks, "##W" + x + " is now online [XCOM]") + else: #Is a regular server player + if x not in XCOMList: + XCOMDelay() #Share its status + whisper(nicks, "##W" + x + " is now online") + + for x in prev: + found = False + for y in curr: + if x==y: found = True + if found == False: + for nicks in XCOMList: #For every XCOM user... + if nicks in online_users.online_users: #That's online... + if nicks in XCOMServerStatInterested: #If XCOM player is interested + if x in XCOMList: #An XCOM user connected? + XCOMDelay() #Share its status + whisper(nicks, "##L" + x + " is now offline [XCOM]") + else: #Is a regular server player + if x not in XCOMList: + XCOMDelay() #Share its status + whisper(nicks, "##L" + x + " is now offline") + +online_users = OnlineUsers(online_url=' https://server.themanaworld.org/online-old.txt', update_interval=20, refresh_hook=online_list_update) + +def XCOMOnlineList(nick, message, is_whisper, match): + XCOMDelay() + msg="" + for nicks in XCOMList: + if nicks in online_users.online_users: + msg = msg + nicks + " | " + XCOMDelay() + whisper(nick, msg) + +def XCOMPrintStat(): + pOnline=0 + xOnline=0 + for p in online_users.online_users: + pOnline=pOnline+1 + if p in XCOMList: + xOnline=xOnline+1 + return "%(xOnline)d/%(pOnline)d"%{"pOnline": pOnline, "xOnline": xOnline,} + +def XCOMDelay(): + time.sleep(0.1) + +def XCOMBroadcast(message): + for nicks in XCOMList: + if nicks in online_users.online_users: + if nicks not in ignored_players: + XCOMDelay() + whisper(nicks, message) + +def XCOMCommunicate(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return #or say something + if message[0]=="!": + return + if message.startswith("*AFK*:"): # AFK bug workaround + return + if nick in XCOMList: + for nicks in XCOMList: + if nicks in online_users.online_users: + if nick==nicks: + pass + else: + XCOMDelay() + whisper(nicks, "##B##LXCOM[" + XCOMPrintStat() + "]##l " + nick + ": ##b" + message) + else: + whisper(nick, XCOMBroadcastPrefix + "XCOM is not enabled (Use !xcon)") + +def XCOMSilentInvite(nick, message, is_whisper, match): + XCOMDelay() + if not is_whisper: + return + if nick in ignored_players: + return #or say something + if nick in admins: + XCOMList.append(match.group(1)) + if match.group(1) not in ignored_players: + whisper(nick, "##W--- " + nick + " silently invited " + match.group(1) + " on XCOM ---") + else: + whisper(nick, "##W" + match.group(1) + " has been ignored by bot and cannot be added to XCOM.") + +def XCOMInvite(nick, message, is_whisper, match): + XCOMDelay() + if not is_whisper: + return + if nick in ignored_players: + return #or say something + if nick in admins: + XCOMList.append(match.group(1)) + XCOMBroadcast("##W--- " + nick + " (Admin) invited " + match.group(1) + " on XCOM ---" + XCOMBroadcastPrefix + match.group(1) + " XCOM enabled! Use !xcoff to disable, use !xclist to see XCOM online list") + else: + if nick in ignored_players: + whisper(nick, "You cannot invite banned players.") + else: + whisper(match.group(1), "##W--- " + nick + " invited you on XCOM --- Answer !xcon to join.") + XCOMDelay() + whisper(nick, "Invited " + match.group(1) + " to join XCOM. Waiting for his/her reply...") + +def XCOMEnable(nick, message, is_whisper, match): + XCOMDelay() + #accept only whispers + if not is_whisper: + return + if nick in ignored_players: + return #or say something + #search array + if nick in XCOMList: + whisper(nick, XCOMBroadcastPrefix + nick + " XCOM already enabled") + else: + XCOMList.append(nick) + XCOMBroadcast("##W--- " + nick + " is online on XCOM ---" + XCOMBroadcastPrefix + nick + " XCOM enabled! Use !xcoff or !xcom off to disable, use !xclist to see XCOM online list") + +def XCOMDisable(nick, message, is_whisper, match): + XCOMDelay() + #accept only whispers + if not is_whisper: + return + if nick in ignored_players: + return #or say something + #search array + if nick in XCOMList: + XCOMBroadcast("##L--- " + nick + " disabled XCOM ---") + XCOMList.remove(nick) + else: + whisper(nick, XCOMBroadcastPrefix + nick + " XCOM already disabled") + +def XCOMServerInterestEnable(nick, message, is_whisper, match): + XCOMDelay() + #accept only whispers + if not is_whisper: + return + if nick in ignored_players: + return #or say something + #search array + if nick in XCOMList: + whisper(nick, XCOMBroadcastPrefix + "Server online status notifications enabled!") + XCOMServerStatInterested.append(nick) + +def XCOMServerInterestDisable(nick, message, is_whisper, match): + XCOMDelay() + #accept only whispers + if not is_whisper: + return + if nick in ignored_players: + return #or say something + #search array + if nick in XCOMList: + whisper(nick, XCOMBroadcastPrefix + "Server online status notifications disabled!") + XCOMServerStatInterested.remove(nick) + +def XCOMBan(nick, message, is_whisper, match): + XCOMDelay() + #accept only whispers + if not is_whisper: + return + if nick in admins: + #search array + if match.group(1) in ignored_players: + whisper(nick, "Already banned.") + else: + ignored_players.append(match.group(1)) + XCOMList.remove(match.group(1)) + #FIXME array need to be saved!!! + XCOMBroadcast(XCOMBroadcastPrefix + match.group(1) + " is now banned from XCOM") + else: + whisper(nick, "Admins only.") + +def XCOMUnBan(nick, message, is_whisper, match): + XCOMDelay() + #accept only whispers + if not is_whisper: + return + if nick in admins: + #search array + if match.group(1) in ignored_players: + XCOMList.append(match.group(1)) + ignored_players.remove(match.group(1)) + #FIXME array need to be saved!!! + XCOMBroadcast(XCOMBroadcastPrefix + match.group(1) + " is now unbanned from XCOM") + whisper(match.group(1), "You are now unbanned from XCOM. Don't make it happen again.") + else: + whisper(nick, "Already banned.") + else: + whisper(nick, "Admins only.") + +# ============================================= + +greetings = { + "Hi {0}!" : 4, + "Hey {0}" : 3, + "Yo {0}" : 2, + "{0}!!!!" : 1, + "{0}!!!" : 1, + "{0}!!" : 1, + "Hello {0}!!!" : 5, + "Hello {0}!" : 5, + "Welcome back {0}!" : 3, + "Hello {0}! You are looking lovely today!" : 1, + "Hello {0}! I'm the bot that you can trust: I want your money!" : 1, + "{0} is back!!" : 1, + "Hello and welcome to the Aperture Science \ +computer-aided enrichment center." : 1, +} + +drop_items = [ + "a bomb", "a bowl of petunias", "a cake", "a candy", "a chocobo", + "a coin", "a cookie", "a drunken pirate", "a freight train", + "a fruit", "a mouboo", "an angry cat", + "an angry polish spelling of a rare element with the atomic number 78", + "an anvil", "an apple", "an iten", "a magic eightball", "a GM", + "a whale", "an elephant", "a piano", "a piece of moon rock", "a pin", + "a rock", "a tub", "a wet mop", "some bass", "Voldemort", "a sandworm", + "a princess", "a prince", "an idea", "Luvia", "a penguin", + "The Hitchhiker's Guide to the Galaxy", +] + +dropping_other = [ + "Hu hu hu.. {0} kicked me!", + "Ouch..", + "Ouchy..", + "*drops dead*", + "*sighs*", + "Leave me alone.", + "Whoa, dammit!", +] + +explain_sentences = { + "livio" : "He created Liviobot.", + "party" : "Is a group of players with their chat tab and they can share exp, items and HP status. See [[@@https://wiki.themanaworld.org/index.php/Legacy:Party_Skill|Party Wiki@@] for more informations.", +} + +dropping_special = { + "ShaiN2" : "*drops a nurse on {0}*", + "Shainen" : "*drops a nurse on {0}*", + "Silent Dawn" : "*drops a box of chocolate on {0}*", + "veryape" : "*drops a chest of rares on {0}*", + "veryapeGM" : "*drops a chest of rares on {0}*", + "Ginaria" : "*drops a bluepar on {0}*", + "Rift Avis" : "*drops an acorn on {0}*", +} + +die_answers = [ + "Avada Kedavra!", + "Make me!", + "Never!!", + "You die, {0}!", + "You die, {0}!", + "You die, {0}!", + "You die, {0}!", + "No!", + "In a minute..", + "Suuure... I'll get right on it", +] + +healme_answers = [ + "Eat an apple, they're good for you.", + "If I do it for you, then I have to do it for everybody.", + "Oh, go drink a potion or something.", + "Whoops! I lost my spellbook.", + "No mana!", +] + +whoami_answers = [ + "An undercover GM.", + "An exiled GM.", + "I'm not telling you!", + "I'm a bot! I'll be level 135 one day! Mwahahahaaha!!!111!", + "Somebody said I'm a Chinese copy of Confused Tree", + "I am your evil twin.", + "I don't remember anything after I woke up! What happened to me?", + "I don't know. Why am I here??", + "Who are you?", + "On the 8th day, God was bored and said 'There will be bots'. \ +So here I am.", + "♪ I'm your hell, I'm your dream, I'm nothing in between ♪♪", + "♪♪ Aperture Science. We do what we must, because.. we can ♪", + "I'm just a reincarnation of a copy.", +] + +burn_answers = [ + "*curses {0} and dies %%c*", + "Help! I'm on fire!", + "Oh hot.. hot hot!", + "*is glowing*", + "*is flaming*", + "ehemm. where are firefighters? I need them now!", + "*is so hot!*", +] + +noidea_answers = [ + "What?", "What??", "Whatever...", "Hmm...", "Huh?", "*yawns*", + "Wait a minute...", "What are you talking about?", + "Who are you?", "What about me?", + "I don't know what you are talking about", + "Excuse me?", "Very interesting", "Really?", + "Go on...", "*Scratches its leafy head*", + "*feels a disturbance in the force*", + "*senses a disturbance in the force*", + "*humming*", "I'm bored..", "%%j", "%%U", "%%[", +] + +pain_answers = [ "Ouch..", "Ouchy..", "Argh..", "Eckk...", "*howls*", + "*screams*", "*groans*", "*cries*", "*faints*", "%%k", + "Why.. What did I do to you? %%i" ] + +hurt_actions = [ "eat", "shoot", "pluck", "torture", "slap", "poison", + "break", "stab", "throw", "drown" ] + +like_answers = [ + "Yay it's", + "You are the sunshine in this beautiful land", + "Can't do this because I like you", +] + +dislike_answers = [ + "Oh, no! It's you!", + "Go away!!!", + "I don't want to see!", + "Your face makes onions cry.", + "You look like I need another drink…", + "Mayas were right...", +] + +bye_answers = [ + "See you soon!", + "Come back anytime!!!", + "See ya!", + "Hope to see you again!", + "More beer for me." +] + + +dislikebye_answers = [ + "Finally!", + "Go away!!!", + "Don't come back!", + "Whew...", + "I need another drink…" +] + +attack_answers = [ + "Attack!!!", + "Whoa, dammit!", + "Alright!!!", + "I have some pain to deliver.", + "Fire at will!!!", + "...I'm all out of gum.", + "His name is: JOHN CENA!!!", + "Target acquired!", + "Puah!", + "Grr!!!", + "Eat this!", + "Ha!", + "Come on!", + "Party time!!!", + "I will burn you down.", + "The show begins...", + "I'm better than makeup artists, prepare yourself!!!", +] + +notattack_answers = [ + "Nope!", + "*picking his nose*", + "Do it yourself.", + "Meh.", + "I will attack you instead.", + "What about my reward?", +] + +story_introductions = [ + "I was with", + "Yesterday I got bored and called", + "With none else around I've asked", +] + +story_action_fail = [ + "failed at it", + "stomped on the soul menhir", + "slipped on a terranite ore", + "got interrupted by phone call", +] + +# FIXME Unused +story_actions = [ + "jumping on", + "speaking with", + "attacking", + "poking", + "playing cards", +] + +# ----------------------------------------------------------------------------- +def say_greeting(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + total_weight = 0 + for w in greetings.itervalues(): + total_weight += w + + random_weight = random.randint(0, total_weight) + total_weight = 0 + random_greeting = 'Hi {0}' + for g, w in greetings.iteritems(): + if total_weight >= random_weight: + random_greeting = g + break + total_weight += w + if nick in disliked_players: + mapserv.cmsg_chat_message(random.choice(dislike_answers)) + else: + mapserv.cmsg_chat_message(random_greeting.format(nick)) + time.sleep(1) + +def say_goodbye(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + total_weight = 0 + for w in greetings.itervalues(): + total_weight += w + + random_weight = random.randint(0, total_weight) + total_weight = 0 + random_greeting = 'Hi {0}' + for g, w in greetings.iteritems(): + if total_weight >= random_weight: + random_greeting = g + break + total_weight += w + if nick in disliked_players: + mapserv.cmsg_chat_message(random.choice(dislikebye_answers)) + else: + mapserv.cmsg_chat_message(random.choice(bye_answers)) + time.sleep(1) + + +def drop_on_head(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + answer = 'yeah' + if nick in dropping_special: + answer = dropping_special[nick] + else: + r = random.randint(0, len(drop_items) + len(dropping_other)) + if r < len(drop_items): + answer = "*drops {} on {}'s head*".format(drop_items[r], nick) + else: + answer = random.choice(dropping_other) + + mapserv.cmsg_chat_message(answer.format(nick)) + + +def answer_threat(nick, _, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + answer = random.choice(die_answers) + mapserv.cmsg_chat_message(answer.format(nick)) + + +# ----------------------------------------------------------------------------- +def admin_additem(nick, _, is_whisper, match): + if not is_whisper: + return + + if nick not in tree_admins: + return + + item = match.group(1) + if item not in drop_items: + drop_items.append(item) + + send_whisper(nick, "Added item '{}' to drop list".format(item)) + + +def admin_addjoke(nick, _, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in tree_admins: + return + + joke = match.group(1) + if joke not in joke_answers: + joke_answers.append(joke) + + send_whisper(nick, "Added joke") + + +# ----------------------------------------------------------------------------- + +PLUGIN = { + 'name': 'manaboy', + 'requires': ('chatbot', 'npc', 'autofollow'), + 'blocks': (), +} + +npcdialog = { + 'start_time': -1, + 'program': [], +} + +_times = { + 'follow': 0, + 'where' : 0, + 'status' : 0, + 'inventory' : 0, + 'say' : 0, + 'zeny' : 0, + 'storage' : 0, +} + +allowed_drops = [535, 719, 513, 727, 729, 869] +allowed_sells = [531, 521, 522, 700, 1201] + +npc_owner = '' +history = deque(maxlen=10) +storage_is_open = False +bugs = deque(maxlen=100) + + +def set_npc_owner(nick): + global npc_owner + # if plugins.npc.npc_id < 0: + npc_owner = nick + + +@extends('smsg_being_remove') +def bot_dies(data): + if data.id == charserv.server.account: + mapserv.cmsg_player_respawn() + + +@extends('smsg_player_chat') +def player_chat(data): + if not npc_owner: + return + + whisper(npc_owner, data.message) + + +@extends('smsg_npc_message') +@extends('smsg_npc_choice') +@extends('smsg_npc_close') +@extends('smsg_npc_next') +@extends('smsg_npc_int_input') +@extends('smsg_npc_str_input') +def npc_activity(data): + npcdialog['start_time'] = time.time() + + +@extends('smsg_npc_message') +def npc_message(data): + if not npc_owner: + return + + npc = mapserv.beings_cache.findName(data.id) + m = '[npc] {} : {}'.format(npc, data.message) + whisper(npc_owner, m) + + +@extends('smsg_npc_choice') +def npc_choice(data): + if not npc_owner: + return + + choices = filter(lambda s: len(s.strip()) > 0, + data.select.split(':')) + + whisper(npc_owner, '[npc][select] (use !input <number> to select)') + for i, s in enumerate(choices): + whisper(npc_owner, ' {}) {}'.format(i + 1, s)) + + +@extends('smsg_npc_int_input') +@extends('smsg_npc_str_input') +def npc_input(data): + if not npc_owner: + return + + t = 'number' + if plugins.npc.input_type == 'str': + t = 'string' + + whisper(npc_owner, '[npc][input] (use !input <{}>)'.format(t)) + + +@extends('smsg_storage_status') +def storage_status(data): + global storage_is_open + storage_is_open = True + _times['storage'] = time.time() + if npc_owner: + whisper(npc_owner, '[storage][{}/{}]'.format( + data.used, data.max_size)) + + +@extends('smsg_storage_items') +def storage_items(data): + if not npc_owner: + return + + items_s = [] + for item in data.storage: + s = itemdb.item_name(item.id, True) + if item.amount > 1: + s = str(item.amount) + ' ' + s + items_s.append(s) + + for l in status.split_names(items_s): + whisper(npc_owner, l) + + +@extends('smsg_storage_equip') +def storage_equipment(data): + if not npc_owner: + return + + items_s = [] + for item in data.equipment: + s = itemdb.item_name(item.id, True) + items_s.append(s) + + for l in status.split_names(items_s): + whisper(npc_owner, l) + + +@extends('smsg_storage_close') +def storage_close(data): + global storage_is_open + storage_is_open = False + _times['storage'] = 0 + + +@extends('smsg_player_arrow_message') +def arrow_message(data): + if npc_owner: + if data.code == 0: + whisper(npc_owner, "Equip arrows") + + +def cmd_where(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + msg = status.player_position() + whisper(nick, msg) + + +def cmd_goto(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + try: + x = int(match.group(1)) + y = int(match.group(2)) + except ValueError: + return + + set_npc_owner(nick) + plugins.autofollow.follow = '' + mapserv.cmsg_player_change_dest(x, y) + + +def cmd_goclose(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + + x = mapserv.player_pos['x'] + y = mapserv.player_pos['y'] + + if message.startswith('!left'): + x -= 1 + elif message.startswith('!right'): + x += 1 + elif message.startswith('!up'): + y -= 1 + elif message.startswith('!down'): + y += 1 + + set_npc_owner(nick) + plugins.autofollow.follow = '' + mapserv.cmsg_player_change_dest(x, y) + + +def cmd_pickup(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + commands.pickup() + + +def cmd_drop(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + + try: + amount = int(match.group(1)) + item_id = int(match.group(2)) + except ValueError: + return + + if nick not in admins: + if item_id not in allowed_drops: + return + + index = get_item_index(item_id) + if index > 0: + mapserv.cmsg_player_inventory_drop(index, amount) + + +def cmd_item_action(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + try: + itemId = int(match.group(1)) + except ValueError: + return + + index = get_item_index(itemId) + if index <= 0: + return + + if message.startswith('!equip'): + mapserv.cmsg_player_equip(index) + elif message.startswith('!unequip'): + mapserv.cmsg_player_unequip(index) + elif message.startswith('!use'): + mapserv.cmsg_player_inventory_use(index, itemId) + + +def cmd_emote(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + try: + emote = int(match.group(1)) + except ValueError: + return + + mapserv.cmsg_player_emote(emote) + + +def cmd_attack(nick, message, is_whisper, match): +# if not is_whisper: +# return + if nick in ignored_players: + return + if nick not in admins: + mapserv.cmsg_chat_message(random.choice(notattack_answers)) + return + + target_s = match.group(1) + + try: + target = mapserv.beings_cache[int(target_s)] + except (ValueError, KeyError): + target = find_nearest_being(name=target_s, + ignored_ids=walkto.unreachable_ids) + + if target in ignored_players: + return + + if target is not None: + set_npc_owner(nick) + plugins.autofollow.follow = '' + if target_s=="Bee": + mapserv.cmsg_chat_message("Forget it " + nick + "!!!") + elif target_s=="Pink Flower": + mapserv.cmsg_chat_message("Yeah, I love those.") + elif target_s=="Squirrel": + mapserv.cmsg_chat_message("Die, you rodent!!!") + mapserv.cmsg_player_emote(5) + walkto.walkto_and_action(target, 'attack', mapserv.player_attack_range) + time.sleep(5) + mapserv.cmsg_chat_message("Go to squirrel's heaven.") + elif target_s in friends: + mapserv.cmsg_chat_message(random.choice(like_answers)+ " " + target_s + "!") + time.sleep(5) + mapserv.cmsg_player_emote(32) + else: + mapserv.cmsg_chat_message(random.choice(attack_answers)) + time.sleep(1) + walkto.walkto_and_action(target, 'attack', mapserv.player_attack_range) + time.sleep(5) + mapserv.cmsg_chat_message(random.choice(attack_answers)) + else: + mapserv.cmsg_chat_message(random.choice(noidea_answers)) + +def cmd_come(nick, message, is_whisper, match): + if nick in ignored_players: + return + if nick not in admins: + mapserv.cmsg_chat_message(random.choice(notattack_answers)) + return + + target_s = match.group(1) + + try: + target = mapserv.beings_cache[int(nick)] + except (ValueError, KeyError): + target = find_nearest_being(name=nick, + ignored_ids=walkto.unreachable_ids) + + if target is not None: + set_npc_owner(nick) + plugins.autofollow.follow = '' + walkto.walkto_and_action(target, '', mapserv.player_attack_range) + mapserv.cmsg_chat_message(random.choice(attack_answers)) + else: + mapserv.cmsg_chat_message(random.choice(noidea_answers)) + +def say_explain(nick, msg, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + + if msg.split(' ',1)[1].lower() in explain_sentences: + mapserv.cmsg_chat_message(explain_sentences[msg.split(' ',1)[1].lower()]) + mapserv.cmsg_player_emote(3) + else: + mapserv.cmsg_chat_message(random.choice(noidea_answers)) +# mapserv.cmsg_chat_message(msg.split(' ',1)[1].lower()) + +def say_think(nick, msg, is_whisper, match): + if is_whisper: + return + + if nick in ignored_players: + return + random_weight = random.randint(0, 2) + if random_weight == 0: + mapserv.cmsg_chat_message(random.choice(noidea_answers)) + if random_weight == 1: + mapserv.cmsg_chat_message("Maybe " + nick + " " + random.choice(hurt_actions) + " " + msg.split(' ')[-1][:-1]+"?") + if random_weight == 2: + mapserv.cmsg_chat_message(nick + " I have to check the wiki.") +# mapserv.cmsg_chat_message(msg.split(' ')[-1][:-1]) + +def make_story(self): + return "asd" + +def say_story(nick, msg, is_whisper, match): + if nick in ignored_players: + return + # ~ random_weight = random.randint(0, 2) + # ~ if random_weight == 0: + # ~ mapserv.cmsg_chat_message(random.choice(noidea_answers)) + # ~ if random_weight == 1: + # ~ mapserv.cmsg_chat_message("Maybe " + nick + " " + random.choice(hurt_actions) + " " + msg.split(' ')[-1][:-1]+"?") + # ~ if random_weight == 2: + # ~ mapserv.cmsg_chat_message(nick + " I have to check the wiki.") + players = [] + for being in mapserv.beings_cache.itervalues(): + if ((being.type == 'player' or being.type == 'npc') and len(being.name) > 1): + players.append(being.name) + monsters = ["monster"] + for being in mapserv.beings_cache.itervalues(): + if being.type == 'monster' and len(being.name) > 1: + monsters.append(being.name) + mapserv.cmsg_chat_message(random.choice(story_introductions) + " " + random.choice(players) + " to " + random.choice(hurt_actions) + " a " + random.choice(monsters) + " with " + random.choice(drop_items) + " but " + random.choice(story_action_fail) +" and said: \"" + random.choice(pain_answers) + "\". Then the " + random.choice(monsters) + " said: \"" + random.choice(noidea_answers) + "\". But " + random.choice(players) +" replied: \"" + random.choice(attack_answers) + "\"") + #mapserv.cmsg_chat_message() + +# Doesn't work. +def cmd_say(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + set_npc_owner(nick) + msg = message.group(1) + mapserv.cmsg_chat_message(msg) + + +def cmd_sit(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + plugins.autofollow.follow = '' + mapserv.cmsg_player_change_act(0, 2) + + +def cmd_turn(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + commands.set_direction('', message[6:]) + + +def cmd_follow(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + if plugins.autofollow.follow == nick: + plugins.autofollow.follow = '' + else: + set_npc_owner(nick) + plugins.autofollow.follow = nick + + +def cmd_lvlup(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + stat = match.group(1).lower() + stats = {'str': 13, 'agi': 14, 'vit': 15, + 'int': 16, 'dex': 17, 'luk': 18} + + skills = {'mallard': 45, 'brawling': 350, 'speed': 352, + 'astral': 354, 'raging': 355, 'resist': 353} + + if stat in stats: + mapserv.cmsg_stat_update_request(stats[stat], 1) + elif stat in skills: + mapserv.cmsg_skill_levelup_request(skills[stat]) + +#FIXME it fails: leads bot to spam +def cmd_invlist(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + ls = status.invlists(50) + for l in ls: + whisper(nick, l) + time.delay(2) + +#FIXME it fails +def cmd_inventory(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + ls = status.invlists2(255) + for l in ls: + whisper(nick, l) + time.delay(2) + + +def cmd_status(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + all_stats = ('stats', 'hpmp', 'weight', 'points', + 'zeny', 'attack', 'skills') + + sr = status.stats_repr(*all_stats) + whisper(nick, ' | '.join(sr.values())) + + +def cmd_zeny(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + whisper(nick, 'I have {} GP'.format(mapserv.player_stats[stats.MONEY])) + + +def cmd_nearby(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + btype = message[8:] + if btype.endswith('s'): + btype = btype[:-1] + + ls = status.nearby(btype) + for l in ls: + whisper(nick, l) + + +def cmd_talk2npc(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + npc_s = match.group(1).strip() + jobs = [] + name = '' + try: + jobs = [int(npc_s)] + except ValueError: + name = npc_s + + b = find_nearest_being(name=name, type='npc', allowed_jobs=jobs) + if b is None: + whisper(nick, '[error] NPC not found: {}'.format(npc_s)) + return + + set_npc_owner(nick) + plugins.autofollow.follow = '' + plugins.npc.npc_id = b.id + mapserv.cmsg_npc_talk(b.id) + + +def cmd_input(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + plugins.npc.cmd_npcinput('', match.group(1)) + + +def cmd_close(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + if storage_is_open: + reset_storage() + else: + plugins.npc.cmd_npcclose() + + +def cmd_history(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + for user, cmd in history: + whisper(nick, '{} : {}'.format(user, cmd)) + + +def cmd_store(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + if not storage_is_open: + return + + try: + amount = int(match.group(1)) + item_id = int(match.group(2)) + except ValueError: + return + + index = get_item_index(item_id) + if index > 0: + mapserv.cmsg_move_to_storage(index, amount) + + +def cmd_retrieve(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + if nick not in admins: + return + if not storage_is_open: + return + + try: + amount = int(match.group(1)) + item_id = int(match.group(2)) + except ValueError: + return + + index = get_storage_index(item_id) + if index > 0: + mapserv.cmsg_move_from_storage(index, amount) + + +def cmd_sell(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + try: + amount = int(match.group(1)) + item_id = int(match.group(2)) + npc_s = match.group(3).strip() + except ValueError: + return + + if item_id not in allowed_sells: + return + + index = get_item_index(item_id) + if index < 0: + return + + jobs = [] + name = '' + try: + jobs = [int(npc_s)] + except ValueError: + name = npc_s + + b = find_nearest_being(name=name, type='npc', allowed_jobs=jobs) + if b is None: + return + + mapserv.cmsg_npc_buy_sell_request(b.id, 1) + mapserv.cmsg_npc_sell_request(index, amount) + + +def cmd_help(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + + m = ('[@@https://forums.themanaworld.org/viewtopic.php?f=12&t=19673|Forum@@]' + '[@@https://bitbucket.org/rumly111/manachat|Sources@@] ' + 'Try !commands for list of commands') + whisper(nick, m) + + +def cmd_commands(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + + c = [] + for cmd in manaboy_commands: + if cmd.startswith('!('): + br = cmd.index(')') + c.extend(cmd[2:br].split('|')) + elif cmd.startswith('!'): + c.append(cmd[1:].split()[0]) + + c.sort() + whisper(nick, ', '.join(c)) + + +def cmd_report_bug(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + + bug_s = match.group(1) + bugs.append((nick, bug_s)) + whisper(nick, 'Thank you for your bug report') + + +def cmd_check_bugs(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in ignored_players: + return + + if nick not in admins: + return + + for user, bug in bugs: + whisper(nick, '{} : {}'.format(user, bug)) + + bugs.clear() + + +def reset_storage(): + mapserv.cmsg_storage_close() + mapserv.cmsg_npc_list_choice(plugins.npc.npc_id, 6) + + +# ========================================================================= +def manaboy_logic(ts): + + def reset(): + global npc_owner + npc_owner = '' + npcdialog['start_time'] = -1 + plugins.npc.cmd_npcinput('', '6') + # plugins.npc.cmd_npcclose() + + if storage_is_open and ts > _times['storage'] + 150: + reset_storage() + + if npcdialog['start_time'] <= 0: + return + + if not storage_is_open and ts > npcdialog['start_time'] + 30.0: + reset() + +# ========================================================================= +manaboy_commands = { + '!where' : cmd_where, + '!goto (\d+) (\d+)' : cmd_goto, + '!(left|right|up|down)' : cmd_goclose, + '!pickup' : cmd_pickup, + '!drop (\d+) (\d+)' : cmd_drop, + '!equip (\d+)' : cmd_item_action, + '!unequip (\d+)' : cmd_item_action, + '!use (\d+)' : cmd_item_action, + '!emote (\d+)' : cmd_emote, + '!attack (.+)' : cmd_attack, + '!say ((@|#).+)' : cmd_say, + '!sit' : cmd_sit, + '!turn' : cmd_turn, + '!follow' : cmd_follow, + '!lvlup (\w+)' : cmd_lvlup, + '!inventory' : cmd_inventory, + '!invlist' : cmd_invlist, + '!status' : cmd_status, + '!zeny' : cmd_zeny, + '!nearby' : cmd_nearby, + '!talk2npc (.+)' : cmd_talk2npc, + '!input (.+)' : cmd_input, + '!close' : cmd_close, + '!store (\d+) (\d+)' : cmd_store, + '!retrieve (\d+) (\d+)' : cmd_retrieve, + '!sell (\d+) (\d+) (.+)' : cmd_sell, + '!(help|info)' : cmd_help, + '!commands' : cmd_commands, + '!history' : cmd_history, + '!bug (.+)' : cmd_report_bug, + '!bugs' : cmd_check_bugs, + '!xcon' : XCOMEnable, + '!xcom' : XCOMEnable, + '!xcoff' : XCOMDisable, + '!xcom off' : XCOMDisable, + '!xclist' : XCOMOnlineList, + '!xci (.*)' : XCOMInvite, + '!xcsi (.*)' : XCOMSilentInvite, + '!xcb (.*)' : XCOMBan, + '!xcu (.*)' : XCOMUnBan, + '!xcsion' : XCOMServerInterestEnable, + '!xcsioff' : XCOMServerInterestDisable, + r'(.*)' : XCOMCommunicate, + r'^(?i)explain (.*)': say_explain, + r'^(?i)(hello|hi|hey|heya|hiya|yo) (?i)(livio|liviobot)' : say_greeting, + r'^(?i)(hello|hi|hey|heya|hiya) (?i)(all|everybody|everyone)(.*)' : say_greeting, + r'\*(?i)?((shake|kick)s?) (?i)(livio|liviobot)' : drop_on_head, + r'\*(?i)?(bye|cya|gtg)' : say_goodbye, + r'(?i)(die|go away|\*?((nuke|kill)s?)) (?i)(livio|liviobot)' : answer_threat, + r'^(?i)(livio|liviobot) (?i)Will (.*)' : noidea_answers, + r'^(?i)heal me([ ,]{1,2})(livio|liviobot)' : healme_answers, + r'^(?i)(who|what) are you([ ,]{1,3})(livio|liviobot)' : whoami_answers, + r'^!additem (.*)' : admin_additem, + r'^!addjoke (.*)' : admin_addjoke, + r'\*(?i)?(burn(s?)) (livio|liviobot)' : burn_answers, + r'\*(?i)?(come) (livio|liviobot)' : cmd_come, + r'\*(?i)?(' + '|'.join(hurt_actions) + ')s?(?i)(livio|liviobot)' : pain_answers, + r'^(?i)what do you think about(.*)' : say_think, + '!story': say_story, + '!joke' : joke_answers, +} + + +def chatbot_answer_mod(func): + '''modifies chatbot.answer to remember last 10 commands''' + + def mb_answer(nick, message, is_whisper): + if is_whisper: + history.append((nick, message)) + return func(nick, message, is_whisper) + + return mb_answer + +def init(config): + + online_users.start() + + for cmd, action in manaboy_commands.items(): + plugins.chatbot.add_command(cmd, action) + plugins.chatbot.answer = chatbot_answer_mod(plugins.chatbot.answer) + + logicmanager.logic_manager.add_logic(manaboy_logic) diff --git a/plugins/manaboy.pyc b/plugins/manaboy.pyc Binary files differnew file mode 100644 index 0000000..a712707 --- /dev/null +++ b/plugins/manaboy.pyc diff --git a/plugins/msgqueue.py b/plugins/msgqueue.py new file mode 100644 index 0000000..25fe2b0 --- /dev/null +++ b/plugins/msgqueue.py @@ -0,0 +1,76 @@ +from collections import deque +from loggers import debuglog +from logicmanager import logic_manager + + +__all__ = [ 'PLUGIN', 'init', 'delayed_functions', 'reload_function' ] + + +PLUGIN = { + 'name': 'msgqueue', + 'requires': (), + 'blocks': (), +} + + +reloaded_functions = {} +event_queue = deque() +_times = { 'next_event' : 0 } + +delayed_functions = { + # 'net.mapserv.cmsg_chat_whisper': 7.5, + # 'net.mapserv.cmsg_chat_message': 3.5, +} + + +def delayed_function(func_name, delay): + + def func(*args, **kwargs): + call = (delay, reloaded_functions[func_name], args, kwargs) + event_queue.append(call) + + return func + + +def reload_function(name, delay): + + def recurs_import(name): + m = __import__(name) + for n in name.split('.')[1:]: + m = getattr(m, n) + return m + + ss = name.rsplit('.', 1) + + if len(ss) == 1: + ss.insert(0, 'net.mapserv') + name = 'net.mapserv.' + name + + try: + module = recurs_import(ss[0]) + func_name = ss[1] + reloaded_functions[name] = getattr(module, func_name) + setattr(module, func_name, delayed_function(name, delay)) + debuglog.debug('function %s wrapped with delay %d', name, delay) + + except Exception as e: + debuglog.error('error wrapping function %s: %s', name, e) + + +def msgq_logic(ts): + if len(event_queue): + if ts > _times['next_event']: + delay, func, args, kwargs = event_queue.popleft() + _times['next_event'] = ts + delay + func(*args, **kwargs) + + +def init(config): + section = PLUGIN['name'] + for option in config.options(section): + delayed_functions[option] = config.getfloat(section, option) + + for func_name, delay in delayed_functions.iteritems(): + reload_function(func_name, delay) + + logic_manager.add_logic(msgq_logic) diff --git a/plugins/msgqueue.pyc b/plugins/msgqueue.pyc Binary files differnew file mode 100644 index 0000000..5814be1 --- /dev/null +++ b/plugins/msgqueue.pyc diff --git a/plugins/notify.py b/plugins/notify.py new file mode 100644 index 0000000..ace4e0b --- /dev/null +++ b/plugins/notify.py @@ -0,0 +1,99 @@ + +import os +import re +from kivy.app import App +from kivy.core.audio import SoundLoader +from plyer import notification +import net.mapserv as mapserv +from utils import extends + + +__all__ = [ 'PLUGIN', 'init', 'timeout', 'guard_words' ] + +timeout = 5000 +guard_words = ["test1", "illia", "eyepatch"] +sound = None + + +PLUGIN = { + 'name': 'notify', + 'requires': (), + 'blocks': (), + 'default_config' : { + 'notif_timeout': 7000, + 'notif_sound' : True + } +} + + +def notify(title, message, use_regex): + bNotify = False + if use_regex: + for regex in guard_words: + if regex.search(message): + bNotify = True + break + else: + bNotify = True + + if bNotify: + app = App.get_running_app() + icon = os.path.join(app.directory, app.icon) + notification.notify(title=title, message=message, + timeout=timeout, + app_name=app.get_application_name(), + app_icon=icon) + if sound is not None: + sound.play() + + +@extends('smsg_being_chat') +def being_chat(data): + app = App.get_running_app() + if app.root_window.focus: + return + + notify('General', data.message, True) + + +@extends('smsg_whisper') +def got_whisper(data): + app = App.get_running_app() + if app.root_window.focus: + return + + nick, message = data.nick, data.message + + notify(nick, message, nick == 'guild') + + +@extends('smsg_party_chat') +def party_chat(data): + app = App.get_running_app() + if app.root_window.focus: + return + + nick = mapserv.party_members.get(data.id, str(data.id)) + message = data.message + m = "{} : {}".format(nick, message) + + notify('Party', m, True) + + +def init(config): + global timeout + global sound + global guard_words + + gw = [] + for w in guard_words: + gw.append(re.compile(w, re.IGNORECASE)) + + gw.append(re.compile(config.get('Player', 'charname'), + re.IGNORECASE)) + guard_words = gw + + timeout = config.getint('notify', 'notif_timeout') + + if config.getboolean('notify', 'notif_sound'): + sound = SoundLoader.load('newmessage.wav') diff --git a/plugins/notify.pyc b/plugins/notify.pyc Binary files differnew file mode 100644 index 0000000..db13ee8 --- /dev/null +++ b/plugins/notify.pyc diff --git a/plugins/npc.py b/plugins/npc.py new file mode 100644 index 0000000..5013562 --- /dev/null +++ b/plugins/npc.py @@ -0,0 +1,135 @@ +import net.mapserv as mapserv +from commands import commands, must_have_arg +from loggers import debuglog +from utils import extends +from actor import find_nearest_being + + +__all__ = [ 'PLUGIN', 'init', 'autonext', 'npc_id', 'input_type' ] + + +PLUGIN = { + 'name': 'npc', + 'requires': (), + 'blocks': (), +} + +npc_id = -1 +autonext = True +input_type = '' + + +@extends('smsg_npc_message') +def npc_message(data): + npc = mapserv.beings_cache.findName(data.id) + m = '[npc] {} : {}'.format(npc, data.message) + debuglog.info(m) + + +@extends('smsg_npc_choice') +def npc_choice(data): + global npc_id + global input_type + npc_id = data.id + input_type = 'select' + choices = filter(lambda s: len(s.strip()) > 0, + data.select.split(':')) + debuglog.info('[npc][select]') + for i, s in enumerate(choices): + debuglog.info(' %d) %s', i + 1, s) + + +@extends('smsg_npc_close') +def npc_close(data): + if autonext: + global npc_id + npc_id = -1 + mapserv.cmsg_npc_close(data.id) + else: + debuglog.info('[npc][close]') + + +@extends('smsg_npc_next') +def npc_next(data): + if autonext: + mapserv.cmsg_npc_next_request(data.id) + else: + debuglog.info('[npc][next]') + + +@extends('smsg_npc_int_input') +def npc_int_input(data): + global input_type + input_type = 'int' + debuglog.info('[npc][input] Enter number:') + + +@extends('smsg_npc_str_input') +def npc_str_input(data): + global input_type + input_type = 'str' + debuglog.info('[npc][input] Enter string:') + + +@must_have_arg +def cmd_npctalk(_, arg): + global npc_id + jobs = [] + name = '' + try: + jobs = [int(arg)] + except ValueError: + name = arg + + b = find_nearest_being(name=name, type='npc', allowed_jobs=jobs) + + if b is not None: + npc_id = b.id + mapserv.cmsg_npc_talk(npc_id) + else: + debuglog.warning("NPC %s not found", arg) + + +def cmd_npcclose(*unused): + global npc_id + if npc_id > -1: + mapserv.cmsg_npc_close(npc_id) + npc_id = -1 + + +def cmd_npcnext(*unused): + if npc_id > -1: + mapserv.cmsg_npc_next_request(npc_id) + + +@must_have_arg +def cmd_npcinput(_, arg): + if npc_id < 0: + return + + global input_type + + if input_type in ('int', 'select'): + try: + n = int(arg) + except ValueError, e: + debuglog.error(e.message) + return + + if input_type == 'int': + mapserv.cmsg_npc_int_response(npc_id, n) + + elif input_type == 'str': + mapserv.cmsg_npc_str_response(npc_id, arg) + + elif input_type == 'select': + mapserv.cmsg_npc_list_choice(npc_id, n) + + input_type = '' + + +def init(config): + commands['talk'] = cmd_npctalk + commands['close'] = cmd_npcclose + commands['next'] = cmd_npcnext + commands['input'] = cmd_npcinput diff --git a/plugins/npc.pyc b/plugins/npc.pyc Binary files differnew file mode 100644 index 0000000..b0dec22 --- /dev/null +++ b/plugins/npc.pyc diff --git a/plugins/restapi.py b/plugins/restapi.py new file mode 100644 index 0000000..5b1d0eb --- /dev/null +++ b/plugins/restapi.py @@ -0,0 +1,149 @@ +import asyncore +import asynchat +import socket +import logging +import json +from collections import deque +from BaseHTTPServer import BaseHTTPRequestHandler + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +import commands + +__all__ = [ 'PLUGIN', 'init' ] + +PLUGIN = { + 'name': 'restapi', + 'requires': (), + 'blocks': (), + 'default_config' : { + 'rest_ip': '127.0.0.1', + 'rest_port': '8971', + } +} + +last_id = 0 +log_history = deque(maxlen=200) + + +def collect_messages(since_id): + ss = [] + for m_id, m_text in log_history: + if m_id >= since_id: + ss.append({'id': m_id, 'message': m_text}) + + return json.dumps(ss) + + +class RequestHandler(asynchat.async_chat, BaseHTTPRequestHandler): + + protocol_version = "HTTP/1.1" + + def __init__(self, conn, addr, server): + asynchat.async_chat.__init__(self, conn) + self.client_address = addr + self.connection = conn + self.server = server + self.set_terminator('\r\n\r\n') + self.rfile = StringIO() + self.wfile = StringIO() + self.found_terminator = self.handle_request_line + + def collect_incoming_data(self, data): + """Collect the data arriving on the connexion""" + self.rfile.write(data) + + def prepare_POST(self): + """Prepare to read the request body""" + bytesToRead = int(self.headers.getheader('Content-Length')) + # set terminator to length (will read bytesToRead bytes) + self.set_terminator(bytesToRead) + self.rfile = StringIO() + # control will be passed to a new found_terminator + self.found_terminator = self.handle_post_data + + def handle_post_data(self): + """Called when a POST request body has been read""" + self.rfile.seek(0) + self.do_POST() + self.finish() + + def handle_request_line(self): + """Called when the http request line and headers have been received""" + self.rfile.seek(0) + self.raw_requestline = self.rfile.readline() + self.parse_request() + + if self.command == 'GET': + self.do_GET() + self.finish() + elif self.command == 'POST': + self.prepare_POST() + else: + self.send_error(501) + + def finish(self): + data = self.wfile.getvalue() + self.push(data) + self.close_when_done() + + def do_GET(self): + try: + since_id = int(self.path[1:]) + except ValueError: + self.send_error(400) + return + + response = collect_messages(since_id) + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(response) + + def do_POST(self): + cmd = self.rfile.getvalue() + commands.process_line(cmd) + self.send_response(200) + + +class Server(asyncore.dispatcher): + + def __init__(self, ip, port, handler): + asyncore.dispatcher.__init__(self) + self.handler = handler + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((ip, port)) + self.listen(5) + + def handle_accept(self): + try: + conn, addr = self.accept() + except: + self.log_info('handle_accept() error', 'warning') + return + + self.handler(conn, addr, self) + + +class RestDebugLogHandler(logging.Handler): + def emit(self, record): + global last_id + msg = self.format(record) + last_id += 1 + log_history.append((last_id, msg)) + + +def init(config): + debuglog = logging.getLogger("ManaChat.Debug") + dbgh = RestDebugLogHandler() + dbgh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%H:%M:%S")) + debuglog.addHandler(dbgh) + + ip = config.get(PLUGIN['name'], 'rest_ip') + port = config.getint(PLUGIN['name'], 'rest_port') + Server(ip, port, RequestHandler) diff --git a/plugins/shop.py b/plugins/shop.py new file mode 100644 index 0000000..44c5db8 --- /dev/null +++ b/plugins/shop.py @@ -0,0 +1,551 @@ +import os +import time +import logging +from collections import OrderedDict +import net.mapserv as mapserv +import chatbot +import logicmanager +import status +import badge +from net.inventory import get_item_index +from net.trade import reset_trade_state +from utils import encode_str, extends +from itemdb import item_name +from playerlist import PlayerList +from chat import send_whisper as whisper + + +__all__ = [ 'PLUGIN', 'init', 'shoplog', 'buying', 'selling' ] + +#nobuy = ['Manatauro', 'ManatauroMage', 'Manatauro Shop', 'kytty'] +nobuy = ['kirito123'] + +PLUGIN = { + 'name': 'shop', + 'requires': ('chatbot',), + 'blocks': (), + 'default_config' : { + 'timeout' : 60, + 'shoplist_txt' : 'shoplist.txt', + 'admins_file' : 'shopAdmins.txt' + } +} + +shoplog = logging.getLogger('ManaChat.Shop') +trade_timeout = 60 +shop_admins = None + + +class s: + player = '' + mode = '' + item_id = 0 + amount = 0 + price = 0 + index = 0 + start_time = 0 + + +buying = OrderedDict([ +# (621, (5000, 1)), # Eyepatch +# (640, (1450, 100)), # Iron Ore +# (4001, (650, 300)), # Coal +]) + +selling = OrderedDict([ +# (535, (100, 50)), # Red Apple +# (640, (1750, 100)), # Iron Ore +]) + + +def cleanup(): + s.player = '' + s.mode = '' + s.item_id = 0 + s.amount = 0 + s.price = 0 + s.index = 0 + s.start_time = 0 + + +# ========================================================================= +def selllist(nick, message, is_whisper, match): + if not is_whisper: + return + if nick in nobuy: + mapserv.cmsg_chat_message("Special prize for you, " + nick + "!") + time.sleep(5) + # ~ return + # Support for 4144's shop (Sell list) + data = '\302\202B1' + + for id_, (price, amount) in selling.iteritems(): + index = get_item_index(id_) + if index < 0: + continue + + _, curr_amount = mapserv.player_inventory[index] + amount = min(curr_amount, amount) + if nick in nobuy: + price=price*100 + + data += encode_str(id_, 2) + data += encode_str(price, 4) + data += encode_str(amount, 3) + + whisper(nick, data) + + +def buylist(nick, message, is_whisper, match): + if not is_whisper: + return + + # Support for 4144's shop (Sell list) + data = '\302\202S1' + + for id_, (price, amount) in buying.iteritems(): + index = get_item_index(id_) + if index > 0: + _, curr_amount = mapserv.player_inventory[index] + amount -= curr_amount + + try: + can_afford = mapserv.player_money / price + except ZeroDivisionError: + can_afford = 10000000 + + amount = min(can_afford, amount) + + if amount <= 0: + continue + + data += encode_str(id_, 2) + data += encode_str(price, 4) + data += encode_str(amount, 3) + + whisper(nick, data) + + +def sellitem(nick, message, is_whisper, match): + if not is_whisper: + return + + item_id = amount = 0 + + # FIXME: check if amount=0 or id=0 + try: + item_id = int(match.group(1)) + # price = int(match.group(2)) + amount = int(match.group(3)) + if item_id < 1 or amount < 1: + raise ValueError + except ValueError: + whisper(nick, "usage: !sellitem ID PRICE AMOUNT") + return + + if s.player: + whisper(nick, "I am currently trading with someone") + return + + player_id = mapserv.beings_cache.findId(nick) + if player_id < 0: + whisper(nick, "I don't see you nearby") + return + + if item_id not in buying: + whisper(nick, "I don't buy that") + return + + real_price, max_amount = buying[item_id] + + index = get_item_index(item_id) + if index > 0: + _, curr_amount = mapserv.player_inventory[index] + max_amount -= curr_amount + + if amount > max_amount: + whisper(nick, "I don't need that many") + return + + total_price = real_price * amount + if total_price > mapserv.player_money: + whisper(nick, "I can't afford it") + return + + s.player = nick + s.mode = 'buy' + s.item_id = item_id + s.amount = amount + s.price = total_price + s.index = index + s.start_time = time.time() + + mapserv.cmsg_trade_request(player_id) + + +def buyitem(nick, message, is_whisper, match): + if not is_whisper: + return + item_id = amount = 0 + + # FIXME: check if amount=0 or id=0 + try: + item_id = int(match.group(1)) + # price = int(match.group(2)) + amount = int(match.group(3)) + if item_id < 1 or amount < 1: + raise ValueError + except ValueError: + whisper(nick, "usage: !buyitem ID PRICE AMOUNT") + return + + if s.player: + whisper(nick, "I am currently trading with someone") + return + + player_id = mapserv.beings_cache.findId(nick) + if player_id < 0: + whisper(nick, "I don't see you nearby") + return + + if item_id not in selling: + whisper(nick, "I don't sell that") + return + + real_price, max_amount = selling[item_id] + + index = get_item_index(item_id) + if index > 0: + _, curr_amount = mapserv.player_inventory[index] + max_amount = min(max_amount, curr_amount) + else: + max_amount = 0 + + if amount > max_amount: + whisper(nick, "I don't have that many") + return + + total_price = real_price * amount + + s.player = nick + s.mode = 'sell' + s.item_id = item_id + s.amount = amount + s.price = total_price + s.index = index + s.start_time = time.time() + + mapserv.cmsg_trade_request(player_id) + + +def retrieve(nick, message, is_whisper, match): + if not is_whisper: + return + + if shop_admins is None: + return + + if not shop_admins.check_player(nick): + return + + item_id = amount = 0 + + try: + item_id = int(match.group(1)) + amount = int(match.group(2)) + if amount < 1: + raise ValueError + except ValueError: + whisper(nick, "usage: !retrieve ID AMOUNT (ID=0 for money)") + return + + if s.player: + whisper(nick, "I am currently trading with someone") + return + + player_id = mapserv.beings_cache.findId(nick) + if player_id < 0: + whisper(nick, "I don't see you nearby") + return + + index = max_amount = 0 + + if item_id == 0: + max_amount = mapserv.player_money + else: + index = get_item_index(item_id) + if index > 0: + max_amount = mapserv.player_inventory[index][1] + + if amount > max_amount: + whisper(nick, "I don't have that many") + return + + s.player = nick + s.mode = 'retrieve' + s.item_id = item_id + s.amount = amount + s.index = index + s.start_time = time.time() + + mapserv.cmsg_trade_request(player_id) + + +def invlist(nick, message, is_whisper, match): + if not is_whisper: + return + + if shop_admins is None: + return + + if not shop_admins.check_player(nick): + return + + ls = status.invlists(50) + for l in ls: + whisper(nick, l) + + +def zeny(nick, message, is_whisper, match): + if not is_whisper: + return + + if shop_admins is None: + return + + if not shop_admins.check_player(nick): + return + + whisper(nick, 'I have {} GP'.format(mapserv.player_stats[20])) + + +# ========================================================================= +@extends('smsg_trade_request') +def trade_request(data): + shoplog.info("Trade request from %s", data.nick) + mapserv.cmsg_trade_response(False) + selllist(data.nick, '', True, None) + + +@extends('smsg_trade_response') +def trade_response(data): + code = data.code + + if code == 0: + shoplog.info("%s is too far", s.player) + whisper(s.player, "You are too far, please come closer") + mapserv.cmsg_trade_cancel_request() # NOTE: do I need it? + cleanup() + + elif code == 3: + shoplog.info("%s accepts trade", s.player) + if s.mode == 'sell': + mapserv.cmsg_trade_item_add_request(s.index, s.amount) + mapserv.cmsg_trade_add_complete() + elif s.mode == 'buy': + mapserv.cmsg_trade_item_add_request(0, s.price) + mapserv.cmsg_trade_add_complete() + elif s.mode == 'retrieve': + mapserv.cmsg_trade_item_add_request(s.index, s.amount) + mapserv.cmsg_trade_add_complete() + else: + shoplog.error("Unknown shop state: %s", s.mode) + mapserv.cmsg_trade_cancel_request() + cleanup() + + elif code == 4: + shoplog.info("%s cancels trade", s.player) + cleanup() + + else: + shoplog.info("Unknown TRADE_RESPONSE code %d", code) + cleanup() + + +@extends('smsg_trade_item_add') +def trade_item_add(data): + item_id, amount = data.id, data.amount + + shoplog.info("%s adds %d %s", s.player, amount, item_name(item_id)) + + if item_id == 0: + return + + if s.mode == 'sell': + whisper(s.player, "I accept only GP") + mapserv.cmsg_trade_cancel_request() + cleanup() + + elif s.mode == 'buy': + if s.item_id != item_id or s.amount != amount: + whisper(s.player, "You should give me {} {}".format( + s.amount, item_name(s.item_id))) + mapserv.cmsg_trade_cancel_request() + cleanup() + + elif s.mode == 'retrieve': + pass + + else: + shoplog.error("Unknown shop state: %s", s.mode) + mapserv.cmsg_trade_cancel_request() + cleanup() + + +@extends('smsg_trade_item_add_response') +def trade_item_add_response(data): + code = data.code + amount = data.amount + + if code == 0: + if amount > 0: + item_id, _ = mapserv.trade_state['items_give'][-1] + shoplog.info("I add to trade %d %s", amount, item_name(item_id)) + + elif code == 1: + shoplog.info("%s is overweight", s.player) + whisper(s.player, "You are overweight") + mapserv.cmsg_trade_cancel_request() + cleanup() + + elif code == 2: + shoplog.info("%s has no free slots", s.player) + whisper(s.player, "You don't have free slots") + mapserv.cmsg_trade_cancel_request() + cleanup() + + else: + shoplog.error("Unknown ITEM_ADD_RESPONSE code: ", code) + mapserv.cmsg_trade_cancel_request() + cleanup() + + +@extends('smsg_trade_cancel') +def trade_cancel(data): + shoplog.error("Trade with %s canceled", s.player) + cleanup() + + +@extends('smsg_trade_ok') +def trade_ok(data): + who = data.who + + if who == 0: + return + + shoplog.info("Trade OK: %s", s.player) + + if s.mode == 'sell': + zeny_get = mapserv.trade_state['zeny_get'] + if zeny_get >= s.price: + mapserv.cmsg_trade_ok() + else: + whisper(s.player, "Your offer makes me sad") + mapserv.cmsg_trade_cancel_request() + cleanup() + + elif s.mode == 'buy': + items_get = {} + for item_id, amount in mapserv.trade_state['items_get']: + try: + items_get[item_id] += amount + except KeyError: + items_get[item_id] = amount + + if s.item_id in items_get and s.amount == items_get[s.item_id]: + mapserv.cmsg_trade_ok() + else: + whisper(s.player, "You should give me {} {}".format( + s.amount, item_name(s.item_id))) + mapserv.cmsg_trade_cancel_request() + cleanup() + + elif s.mode == 'retrieve': + mapserv.cmsg_trade_ok() + + else: + shoplog.error("Unknown shop state: %s", s.mode) + mapserv.cmsg_trade_cancel_request() + cleanup() + + +@extends('smsg_trade_complete') +def trade_complete(data): + if s.mode == 'sell': + shoplog.info("Trade with %s completed. I sold %d %s for %d GP", + s.player, s.amount, item_name(s.item_id), + mapserv.trade_state['zeny_get']) + elif s.mode == 'buy': + shoplog.info("Trade with %s completed. I bought %d %s for %d GP", + s.player, s.amount, item_name(s.item_id), + mapserv.trade_state['zeny_give']) + elif s.mode == 'retrieve': + shoplog.info("Trade with %s completed.", s.player) + else: + shoplog.info("Trade with %s completed. Unknown shop state %s", + s.player, s.mode) + + reset_trade_state(mapserv.trade_state) + + cleanup() + + +# ========================================================================= +def shop_logic(ts): + if s.start_time > 0: + if ts > s.start_time + trade_timeout: + shoplog.warning("%s timed out", s.player) + mapserv.cmsg_trade_cancel_request() + + +# ========================================================================= +shop_commands = { + '!selllist' : selllist, + '!buylist' : buylist, + '!sellitem (\d+) (\d+) (\d+)' : sellitem, + '!buyitem (\d+) (\d+) (\d+)' : buyitem, + '!retrieve (\d+) (\d+)' : retrieve, + '!invlist' : invlist, + '!zeny' : zeny, +} + + +def load_shop_list(config): + global buying + global selling + + shoplist_txt = config.get('shop', 'shoplist_txt') + if not os.path.isfile(shoplist_txt): + shoplog.warning('shoplist file not found : %s', shoplist_txt) + return + + with open(shoplist_txt, 'r') as f: + for l in f: + try: + item_id, buy_amount, buy_price, sell_amount, sell_price = \ + map(int, l.split()) + if buy_amount > 0: + buying[item_id] = buy_price, buy_amount + if sell_amount > 0: + selling[item_id] = sell_price, sell_amount + except ValueError: + pass + + +def init(config): + for cmd, action in shop_commands.items(): + chatbot.add_command(cmd, action) + + global trade_timeout + global shop_admins + + trade_timeout = config.getint('shop', 'timeout') + + shop_admins_file = config.get('shop', 'admins_file') + if os.path.isfile(shop_admins_file): + shop_admins = PlayerList(shop_admins_file) + + badge.is_shop = True + + load_shop_list(config) + logicmanager.logic_manager.add_logic(shop_logic) diff --git a/plugins/shop.pyc b/plugins/shop.pyc Binary files differnew file mode 100644 index 0000000..aa39471 --- /dev/null +++ b/plugins/shop.pyc diff --git a/runshop.py b/runshop.py new file mode 100644 index 0000000..22fd740 --- /dev/null +++ b/runshop.py @@ -0,0 +1,63 @@ +#!/usr/bin/python2 + +import asyncore +import logging +import sys +from ConfigParser import ConfigParser + +try: + import construct + del construct +except ImportError: + import os + sys.path.insert(0, os.path.join(os.getcwd(), "external")) + +import net +import net.mapserv as mapserv +import plugins +from utils import extends +from itemdb import load_itemdb +from loggers import debuglog +from logicmanager import logic_manager + + +@extends('smsg_player_warp') +def player_warp(data): + mapserv.cmsg_map_loaded() + + +@extends('smsg_map_login_success') +def map_login_success(data): + mapserv.cmsg_map_loaded() + + +if __name__ == '__main__': + rootLogger = logging.getLogger('') + rootLogger.addHandler(logging.NullHandler()) + + dbgh = logging.StreamHandler(sys.stdout) + dbgh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S")) + debuglog.addHandler(dbgh) + debuglog.setLevel(logging.INFO) + + shoplog = logging.getLogger('ManaChat.Shop') + shoplog.setLevel(logging.INFO) + shoplog.addHandler(dbgh) + + config = ConfigParser() + config.read('manachat.ini') + + load_itemdb('itemdb.txt') + + plugins.load_plugins(config) + + net.login(host=config.get('Server', 'host'), + port=config.getint('Server', 'port'), + username=config.get('Player', 'username'), + password=config.get('Player', 'password'), + charname=config.get('Player', 'charname')) + + while True: + asyncore.loop(timeout=0.2, count=5) + logic_manager.tick() diff --git a/shopAdmins.txt b/shopAdmins.txt new file mode 100644 index 0000000..ede5d01 --- /dev/null +++ b/shopAdmins.txt @@ -0,0 +1 @@ +Livio diff --git a/shoplist.txt b/shoplist.txt new file mode 100644 index 0000000..56f5dc4 --- /dev/null +++ b/shoplist.txt @@ -0,0 +1,2 @@ +904 0 0 100 1 +1199 30000 1 5000 1 diff --git a/simple.py b/simple.py new file mode 100755 index 0000000..b05ffab --- /dev/null +++ b/simple.py @@ -0,0 +1,138 @@ +#!/usr/bin/python2 + +# ManaChat simple frontend. Works only on *nix + +import asyncore +import logging +import os +import sys +import readline +import thread +import struct +import fcntl +import termios +from collections import deque +from ConfigParser import ConfigParser + +try: + import construct + del construct +except ImportError: + import os + sys.path.insert(0, os.path.join(os.getcwd(), "external")) + +import net +import net.mapserv as mapserv +import plugins +from utils import extends +from itemdb import load_itemdb +from monsterdb import read_monster_db +from loggers import debuglog +import commands +from logicmanager import logic_manager + +PROMPT = '] ' +input_buffer = deque() + + +def input_thread(): + while True: + s = raw_input(PROMPT) + input_buffer.append(s) + + +class DebugLogHandler(logging.Handler): + + def clear_curr_input(self): + # http://stackoverflow.com/questions/2082387/reading-input-from-raw-input-without-having-the-prompt-overwritten-by-other-th?rq=1 + _, cols = struct.unpack('hh', + fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, '1234')) + tl = len(readline.get_line_buffer()) + len(PROMPT) + # ANSI escape sequences (All VT100 except ESC[0G) + sys.stdout.write('\x1b[2K') # Clear current line + sys.stdout.write('\x1b[1A\x1b[2K' * (tl / cols)) # cur up & clr line + sys.stdout.write('\x1b[0G') # to start of line + + def emit(self, record): + self.clear_curr_input() + print self.format(record) + sys.stdout.write(PROMPT + readline.get_line_buffer()) + sys.stdout.flush() + + +@extends('smsg_player_warp') +def player_warp(data): + mapserv.cmsg_map_loaded() + m = "[warp] {} ({},{})".format(data.map, data.x, data.y) + debuglog.info(m) + + +@extends('smsg_map_login_success') +def map_login_success(data): + mapserv.server.raw = True + mapserv.cmsg_map_loaded() + + +@extends('smsg_whisper_response') +def send_whisper_result(data): + if data.code == 0: + if len(readline.get_line_buffer()) == 0: + last_nick = mapserv.last_whisper['to'] + readline.insert_text('/w "{}" '.format(last_nick)) + readline.redisplay() + + +if __name__ == '__main__': + try: + rootLogger = logging.getLogger('') + rootLogger.addHandler(logging.NullHandler()) + + dbgh = DebugLogHandler() + dbgh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S")) + debuglog.addHandler(dbgh) + debuglog.setLevel(logging.INFO) + + config_ini = 'manachat.ini' + if len(sys.argv) > 1: + if sys.argv[1].endswith('.ini') and os.path.isfile(sys.argv[1]): + config_ini = sys.argv[1] + config = ConfigParser() + config.read(config_ini) + + if config.getboolean('Other', 'log_network_packets'): + from loggers import netlog + netlog.setLevel(logging.INFO) + fh = logging.FileHandler('/tmp/netlog.txt', mode="w") + fmt = logging.Formatter("[%(asctime)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S") + fh.setFormatter(fmt) + netlog.addHandler(fh) + + load_itemdb('itemdb.txt') + read_monster_db('monsterdb.txt') + + plugins.load_plugins(config) + + net.login(host=config.get('Server', 'host'), + port=config.getint('Server', 'port'), + username=config.get('Player', 'username'), + password=config.get('Player', 'password'), + charname=config.get('Player', 'charname')) + + thread.start_new_thread(input_thread, ()) + + while True: + if len(input_buffer) > 0: + for l in input_buffer: + commands.process_line(l) + input_buffer.clear() + + asyncore.loop(timeout=0.2, count=5) + logic_manager.tick() + except Exception as e: + print(e) ## Fails on Keyboard interrupt + os._exit(255) + except KeyboardInterrupt: + print("Interrupted by keyboard") + os._exit(255) diff --git a/status.py b/status.py new file mode 100644 index 0000000..6b497a3 --- /dev/null +++ b/status.py @@ -0,0 +1,179 @@ +from collections import OrderedDict +import net.mapserv as mapserv +import net.stats as st +import itemdb +from utils import encode_str +import mapnames + + +def stats_repr(*stat_types): + ps = mapserv.player_stats + sd = OrderedDict() + + if 'stats' in stat_types: + sd['stats'] = 'STR:{} AGI:{} VIT:{} INT:{} DEX:{} LUK:{}'.format( + ps[st.STR], ps[st.AGI], ps[st.VIT], ps[st.INT], + ps[st.DEX], ps[st.LUK]) + + if 'hpmp' in stat_types: + sd['hpmp'] = 'HP:{}/{} MP:{}/{}'.format(ps[st.HP], ps[st.MAX_HP], + ps[st.MP], ps[st.MAX_MP]) + + if 'weight' in stat_types: + sd['weight'] = 'WG: {}/{}'.format(ps[st.TOTAL_WEIGHT], + ps[st.MAX_WEIGHT]) + + if 'points' in stat_types: + sd['points'] = 'LVL: {} EXP:{}/{} CP:{} SP:{}'.format( + ps[st.LEVEL], ps[st.EXP], ps[st.EXP_NEEDED], + ps[st.CHAR_POINTS], ps[st.SKILL_POINTS]) + + if 'zeny' in stat_types: + sd['zeny'] = 'GP:{}'.format(ps[st.MONEY]) + + if 'attack' in stat_types: + sd['attack'] = 'ATK:{} DEF:{} MATK:{} MDEF:{}'.format( + ps[st.ATK], ps[st.DEF], ps[st.MATK], ps[st.MDEF]) + + if 'skills' in stat_types: + sl = [] + ps = mapserv.player_skills + skill_names = {339: 'focusing', 45: 'mallard', 350: 'brawling', + 352: 'speed', 353: 'resist', 354: 'astral', + 355: 'raging', 340: 'magic', 341: 'life', + 342: 'war', 343: 'transmut', 344: 'nature', + 345: 'astralm'} + + for s_id, s_v in ps.items(): + if s_v > 0: + sl.append('{}:{}'.format(skill_names.get(s_id, + str(s_id)), s_v)) + + sd['skills'] = ' '.join(sl) + + return sd + + +def invlists(max_items=1000): + inventory = OrderedDict() + + for id_, amount in mapserv.player_inventory.values(): + inventory[id_] = inventory.setdefault(id_, 0) + amount + + lists = [] + data = '\302\202B1' + i = 0 + for id_, amount in inventory.items(): + i += 1 + if i > max_items: + i = 0 + lists.append(data) + data = '\302\202B1' + data += encode_str(id_, 2) + data += encode_str(1, 4) + data += encode_str(amount, 3) + + lists.append(data) + return lists + + +def invlists2(max_length=255, source='inventory'): + inventory = OrderedDict() + + if source == 'inventory': + source = mapserv.player_inventory + elif source == 'storage': + source = mapserv.player_storage + else: + return [] + + for id_, amount in source.values(): + inventory[id_] = inventory.setdefault(id_, 0) + amount + + lists = [] + data = '' + for id_, amount in inventory.items(): + s = itemdb.item_name(id_, True) + ', ' + if amount > 1: + s = str(amount) + ' ' + s + if len(data + s) > max_length: + lists.append(data) + data = '' + data += s + + lists.append(data[:-2]) + return lists + + +def player_position(): + pp = mapserv.player_pos + map_name = mapnames.map_names.get(pp['map'], 'Unknown') + s = "Map: {} ({}), coor: {}, {}".format( + map_name, pp['map'], pp['x'], pp['y']) + return s + + +def split_names(names, origin=[''], maxlen=255, separator=', '): + origin = origin[:] + if len(origin) < 1: + origin = [''] + + for n in names: + ns = n + separator + if len(origin[-1] + ns) > maxlen: + origin.append(ns) + else: + origin[-1] += ns + + origin[-1] = origin[-1][:-len(separator)] + + return origin + + +def nearby(btype=''): + nearby_beings = {'player': {}, 'npc': {}, 'monster': {}, 'portal': []} + for being in mapserv.beings_cache.itervalues(): + if btype and being.type != btype: + continue + + if being.type == 'portal': + nearby_beings['portal'].append((being.x, being.y)) + continue + + if being.type == 'npc' and len(being.name) < 1: + nearby_beings[being.type][str(being.job)] = 1 + elif being.name in nearby_beings[being.type]: + nearby_beings[being.type][being.name] += 1 + else: + nearby_beings[being.type][being.name] = 1 + + del_types = [] + for t in nearby_beings: + if not nearby_beings[t]: + del_types.append(t) + for t in del_types: + del nearby_beings[t] + + lines = [] + for bt in ('monster', 'player', 'npc'): + if bt not in nearby_beings: + continue + lines.append(bt + 's : ') + names_s = [] + for bname in sorted(nearby_beings[bt].iterkeys()): + count = nearby_beings[bt][bname] + if count > 1: + names_s.append('{} ({})'.format(bname, count)) + else: + names_s.append(bname) + + lines = split_names(names_s, lines) + + if 'portal' in nearby_beings: + lines.append('portals : ') + coor_s = [] + for cx, cy in nearby_beings['portal']: + coor_s.append('({},{})'.format(cx, cy)) + lines = split_names(coor_s, lines) + + return lines diff --git a/status.pyc b/status.pyc Binary files differnew file mode 100644 index 0000000..c4e1ac9 --- /dev/null +++ b/status.pyc diff --git a/textutils.py b/textutils.py new file mode 100644 index 0000000..4cd5b2e --- /dev/null +++ b/textutils.py @@ -0,0 +1,69 @@ +import re + +__all__ = ('preprocess', 'remove_formatting', 'replace_emotes', + 'simplify_links', 'expand_links', 'links_to_markup') + +formatting_re = re.compile(r'##[0-9bB]') +emotes_re = re.compile(r'%%[^%]') +mplus_link_re = re.compile(r'@@([^|]+)\|([^@]+)@@') +url_re = re.compile(r'(^|[^@|])((http|https|ftp)://([^\t ]+))') + + +def remove_formatting(text): + return re.sub(formatting_re, '', text) + + +def replace_emotes(text): + + emotes = ( ":-D", ":-)", ";-)", ":-(", ":-o", ":-|", ":-/", "B-)", + ":-D", ":-[", ":-P", "*blush*", ":'-(", "*:-]*", + "*weird emote*", "*ninja*", ":-)", "*star*", "*?*", "*!*", "*idea*", + "*->*", "<3", "^_^", ":-)", ";-)", ":-(", ":-O", ":-(", + "*mimi*", ":-D", ":-D", "*perturbed*", ":-P", + "*shame*", ":-(", ">:-D", "0_o", "*ninja*", "*bad geek*", + "*star*", "*?*", "*!*", "*bubble*", ">_>", "*in love*", + "*disgust*", ">:-D", ":-(", "xD", "u.u", "x_x", + "*facepalm*", ">:-D", "*angry*", ":-D", "*metal*", + ":'-(", "*...*", "*@:=*", ":3", "*zZzZz*", "-.-'", + "*alien*") + + def emote_repl(m): + code = ord(m.group(0)[2]) - 48 + if code > len(emotes): + return m.group(0) + else: + return emotes[code] + + return re.sub(emotes_re, emote_repl, text) + + +def simplify_links(text): + + def simplify(m): + return m.group(2) + + return re.sub(mplus_link_re, simplify, text) + + +def expand_links(text): + + def expand(m): + return '{}[@@{}|{}@@]'.format(m.group(1), m.group(2), m.group(4)) + + # text = ' ' + text + return re.sub(url_re, expand, text) + + +def links_to_markup(text): + return re.sub(mplus_link_re, + r'[ref=\1][color=2F3Fff]\2[/color][/ref]', + text) + + +def preprocess(text, actions=(simplify_links, + remove_formatting, + replace_emotes)): + for f in actions: + text = f(text) + + return text diff --git a/textutils.pyc b/textutils.pyc Binary files differnew file mode 100644 index 0000000..6fef757 --- /dev/null +++ b/textutils.pyc diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..0416a90 --- /dev/null +++ b/utils.py @@ -0,0 +1,150 @@ +import time +import threading +from loggers import debuglog + + +def log_method(fun): + def wrapper(self, *args, **kwargs): + s_args = s_kwargs = '' + if args: s_args = ','.join(str(a) for a in args) + if kwargs: s_kwargs = ','.join('{}={}'.format(k, v) + for k, v in kwargs.items()) + debuglog.debug('CALL: {}.{}({},{})'.format(self.__class__.__name__, + fun.__name__, + s_args, s_kwargs)) + result = fun(self, *args, **kwargs) + return result + return wrapper + + +def log_function(fun): + def wrapper(*args, **kwargs): + s_args = s_kwargs = '' + if args: s_args = ','.join(str(a) for a in args) + if kwargs: s_kwargs = ','.join('{}={}'.format(k, v) + for k, v in kwargs.items()) + debuglog.debug('CALL: {}({},{})'.format(fun.__name__, + s_args, s_kwargs)) + result = fun(*args, **kwargs) + return result + return wrapper + + +class Schedule: + """Class to schedule repeatable events""" + def __init__(self, start, delay, func, *args, **kwargs): + self._active = True + self._next_event = time.time() + start + self._delay = delay + self._func = func + self._thread = threading.Thread(target=self._threadfunc, + args=args, kwargs=kwargs) + self._thread.start() + + def _threadfunc(self, *args, **kwargs): + while self._active: + now = time.time() + if now >= self._next_event: + self._func(*args, **kwargs) + self._next_event += self._delay + if self._delay <= 0: + return + time.sleep(0.1) + + def cancel(self): + if self._active: + self._active = False + if self._thread is not threading.current_thread(): + self._thread.join() + + +# The following group of functions is to provide a way to extend +# a module's functions by decorating them with @extendable + +_extensions = {} + + +def register_extension(name, func): + if name in _extensions: + if func not in _extensions[name]: + _extensions[name].append(func) + else: + _extensions[name] = [func] + + +def extendable(func): + """ + Decorator function. If a function is decorated with + @extendable, each call of it will be followed by calls of + _extentions[fun.__name__](*args, **kwargs) with same args + """ + + def wrapper(*args, **kwargs): + name = func.__name__ + func(*args, **kwargs) + if name in _extensions: + for f in _extensions[name]: + f(*args, **kwargs) + + wrapper.__name__ = func.__name__ + return wrapper + + +def extends(func_name): + """ + Return a decorator that registers the wrapped function + as an extention to func_name call. + See @extendable. + """ + + def decorator(func): + + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + register_extension(func_name, func) + return wrapper + + return decorator + + +def preprocess_argument(pp_fun, arg=0): + """ + Make a decorator that modifies 1 argument of a given function + before calling it. + pp_fun accepts this argument and returns the modified. + arg can be either int, then args[arg] is modified, or it + can be str, then kwargs[arg] is modified + """ + + def decorator(fun): + + def wrapper(*args, **kwargs): + if isinstance(arg, int): + if arg < len(args): + args = list(args) + args[arg] = pp_fun(args[arg]) + elif isinstance(arg, str): + if arg in kwargs: + kwargs[arg] = pp_fun(kwargs[arg]) + + return fun(*args, **kwargs) + + return wrapper + + return decorator + + +# Encode string - used with 4144 shop compatibility. +def encode_str(value, size): + output = '' + base = 94 + start = 33 + while value: + output += chr(value % base + start) + value /= base + + while len(output) < size: + output += chr(start) + + return output diff --git a/utils.pyc b/utils.pyc Binary files differnew file mode 100644 index 0000000..8f33f46 --- /dev/null +++ b/utils.pyc diff --git a/utils/generate-packet-lengths.py b/utils/generate-packet-lengths.py new file mode 100644 index 0000000..a88882e --- /dev/null +++ b/utils/generate-packet-lengths.py @@ -0,0 +1,54 @@ +#!/usr/bin/python + +""" +Generate a dict of packet lenghts, based on packetsin.inc from ManaPlus. +Author: Joseph Botosh <rumly111@gmail.com> +Licence: GPLv2. +""" + +import os +import sys + + +def GeneratePacketLengths(infile): + plength = {} + with open(infile) as f: + for l in f: + if l.startswith('packet('): + w = l[7:-2].split(',') + opcode = int(w[1].strip(), 16) + length = int(w[2].strip()) + plength[opcode] = length + return plength + + +def PrettyPrint(plengths, width=80): + s = 'packet_lengths = {\n ' + curr_line_len = 4 + for opcode in sorted(plengths.keys()): + r = '0x{:04x}: {:d},'.format(opcode, plengths[opcode]).ljust(12) + if curr_line_len + len(r) > width: + curr_line_len = 4 + s += '\n ' + s += r + curr_line_len += len(r) + s += '}\n' + return s + + +def PrintHelp(): + print('Usage: {} /path/to/packetsin.inc'.format(sys.argv[0])) + +if __name__ == '__main__': + if len(sys.argv) == 1: + PrintHelp() + sys.exit(0) + filename = sys.argv[1] + if os.path.isfile(filename): + pl = GeneratePacketLengths(filename) + print(PrettyPrint(pl)) + # print('packet_lengths = ', + # GeneratePacketLengths(filename)) + else: + print('File not found:', filename, file=sys.stderr) + sys.exit(1) diff --git a/utils/update-item-db.py b/utils/update-item-db.py new file mode 100644 index 0000000..27969de --- /dev/null +++ b/utils/update-item-db.py @@ -0,0 +1,42 @@ +#!/usr/bin/python + +""" +Update text file containing item IDs and names. +Author: Joseph Botosh <rumly111@gmail.com> +Licence: GPLv2. +""" + +import os +import sys +from xml.etree.ElementTree import ElementTree + + +def ScanItemsXML(filename): + result = [] + file1 = ElementTree(file=filename) + for item1 in filter(lambda it: it.get('name'), file1.getroot()): + file2 = ElementTree(file=item1.get('name')) + for item2 in filter(lambda it: it.get('name'), file2.getroot()): + file3 = ElementTree(file=item2.get('name')) + for item3 in file3.getroot(): + name = item3.get('name') + id_ = item3.get('id') + if name is not None and id_ is not None and int(id_) > 0: + result.append((id_, name)) + return result + + +def PrintHelp(): + print('Usage: {} <items.xml>'.format(sys.argv[0])) + +if __name__ == '__main__': + if len(sys.argv) == 1: + PrintHelp() + sys.exit(0) + filename = sys.argv[1] + if os.path.isfile(filename): + for id_, name in ScanItemsXML(filename): + print(id_, name) + else: + print('File not found:', filename, file=sys.stderr) + sys.exit(1) diff --git a/utils/update-map-db.py b/utils/update-map-db.py new file mode 100644 index 0000000..a17f5f1 --- /dev/null +++ b/utils/update-map-db.py @@ -0,0 +1,76 @@ +#!/usr/bin/python2 + +""" +Update pickle file containing maps information. +It is needed, because for some reason pytmx doesn't work +properly on android. + +Author: Joseph Botosh <rumly111@gmail.com> +Licence: GPLv2. +""" + +import os +import sys +import zipfile +import pytmx + +try: + import cPickle as pickle +except: + import pickle + +try: + import zlib + compression = zipfile.ZIP_DEFLATED + del zlib +except: + compression = zipfile.ZIP_STORED + + +modes = { zipfile.ZIP_DEFLATED: 'deflated', + zipfile.ZIP_STORED: 'stored' } + + +def LoadTmxMap(filename, maptag): + m = pytmx.TiledMap(filename=filename) + + tmx = { + 'tag': maptag, + 'name': m.properties['name'], + 'width': m.width, + 'height': m.height, + 'collisions': m.get_layer_by_name('Collision').data + } + + return tmx + + +def PrintHelp(): + print('Usage: {} <maps-dir> <outfile.pickle>'.format(sys.argv[0])) + +if __name__ == '__main__': + if len(sys.argv) < 3: + PrintHelp() + sys.exit(0) + dirname = sys.argv[1] + outfile = sys.argv[2] + + n = 0 + zf = zipfile.ZipFile(outfile, 'w') + + for tmx in (filter(lambda f: f.endswith('.tmx'), + os.listdir(dirname))): + path = os.path.join(dirname, tmx) + maptag = tmx[:-4] + print("Loading map {} ...".format(path)) + try: + c = LoadTmxMap(path, maptag) + print("\tname={} tag={} size=({},{})".format( + c['name'], c['tag'], c['width'], c['height'])) + zf.writestr(maptag + '.pickle', pickle.dumps(c), compression) + n += 1 + except KeyError as e: + print("Error loading {}: {}".format(tmx, e)) + + zf.close() + print("Done processing {} maps".format(n)) diff --git a/utils/update-map-names.py b/utils/update-map-names.py new file mode 100644 index 0000000..8274390 --- /dev/null +++ b/utils/update-map-names.py @@ -0,0 +1,32 @@ +#!/usr/bin/python2 + +""" +Author: Joseph Botosh <rumly111@gmail.com> +Licence: GPLv2. +""" + +import os +import sys +import pytmx + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print('Usage: {} <maps-dir>'.format(sys.argv[0])) + sys.exit(0) + + dirname = sys.argv[1] + + for tmx in (filter(lambda f: f.endswith('.tmx'), + os.listdir(dirname))): + path = os.path.join(dirname, tmx) + maptag = tmx[:-4] + m = pytmx.TiledMap(filename=path) + try: + if 'name' in m.properties: + mapname = m.properties['name'] + else: + mapname = m.properties['Name'] + print maptag, mapname + except KeyError: + print '[error]', maptag diff --git a/utils/update-monster-db.py b/utils/update-monster-db.py new file mode 100644 index 0000000..a77cb81 --- /dev/null +++ b/utils/update-monster-db.py @@ -0,0 +1,42 @@ +#!/usr/bin/python + +""" +Update text file containing monster IDs and names. +Author: Joseph Botosh <rumly111@gmail.com> +Licence: GPLv2. +""" + +import os +import sys +from xml.etree.ElementTree import ElementTree + + +def ScanItemsXML(filename): + result = [] + file1 = ElementTree(file=filename) + for item1 in filter(lambda it: it.get('name'), file1.getroot()): + file2 = ElementTree(file=item1.get('name')) + for item2 in filter(lambda it: it.get('name'), file2.getroot()): + file3 = ElementTree(file=item2.get('name')) + for item3 in file3.getroot(): + name = item3.get('name') + id_ = item3.get('id') + if name is not None and id_ is not None and int(id_) > 0: + result.append((id_, name)) + return result + + +def PrintHelp(): + print('Usage: {} <monsters.xml>'.format(sys.argv[0])) + +if __name__ == '__main__': + if len(sys.argv) == 1: + PrintHelp() + sys.exit(0) + filename = sys.argv[1] + if os.path.isfile(filename): + for id_, name in ScanItemsXML(filename): + print(id_, name) + else: + print('File not found:', filename, file=sys.stderr) + sys.exit(1) diff --git a/walkto.py b/walkto.py new file mode 100644 index 0000000..a2b0312 --- /dev/null +++ b/walkto.py @@ -0,0 +1,110 @@ +import time +import net.mapserv as mapserv +import net.charserv as charserv +import logicmanager +from net.common import distance +from utils import extends + + +__all__ = [ 'unreachable_ids', 'walkto_and_action', 'target', 'state' ] + + +_times = { + 'arrival_time': 0, + 'walk_request_time': 0, +} + +unreachable_ids = set() +target = None +action = '' +state = '' + + +def reset_walkto(): + global state + global target + global action + + state = '' + target = None + action = '' + + +def walkto_and_action(obj, action_, min_distance=1): + if obj.id in unreachable_ids: + return + + global state + global target + global action + + if state == 'waiting_confirmation': + return + + target = obj + action = action_ + + pp = mapserv.player_pos + dist = distance(pp['x'], pp['y'], target.x, target.y) + + if dist <= min_distance: + do_action(target, action) + else: + state = 'waiting_confirmation' + _times['walk_request_time'] = time.time() + mapserv.cmsg_player_change_dest(target.x, target.y) + + +def do_action(target, action): + if action == 'attack': + mapserv.cmsg_player_change_act(target.id, 7) + elif action == 'pickup': + mapserv.cmsg_item_pickup(target.id) + + +def walkto_logic(ts): + global state + + if state == '': + return + elif state == 'waiting_confirmation': + if ts > _times['walk_request_time'] + 0.5: + unreachable_ids.add(target.id) + reset_walkto() + elif state == 'walking': + if ts >= _times['arrival_time']: + state = '' + do_action(target, action) + + +@extends('smsg_being_remove') +@extends('smsg_item_remove') +def target_removed(data): + unreachable_ids.discard(data.id) + if target and target.id == data.id: + reset_walkto() + if data.id == charserv.server.account: + reset_walkto() + + +@extends('smsg_player_warp') +def player_warp(data): + reset_walkto() + unreachable_ids.clear() + + +def calc_walk_time(distance, speed=0.15): + return 0.5 + speed * distance + + +@extends('smsg_walk_response') +def walk_response(data): + global state + if state == 'waiting_confirmation': + state = 'walking' + cp = data.coor_pair + dist = distance(cp.src_x, cp.src_y, cp.dst_x, cp.dst_y) + _times['arrival_time'] = time.time() + calc_walk_time(dist) + + +logicmanager.logic_manager.add_logic(walkto_logic) diff --git a/walkto.pyc b/walkto.pyc Binary files differnew file mode 100644 index 0000000..1a69a47 --- /dev/null +++ b/walkto.pyc |