Merge branch 'master' of https://github.com/peterix/dfhack
| After Width: | Height: | Size: 3.8 KiB | 
| After Width: | Height: | Size: 5.1 KiB | 
| After Width: | Height: | Size: 2.9 KiB | 
| After Width: | Height: | Size: 4.8 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 7.5 KiB | 
| After Width: | Height: | Size: 7.4 KiB | 
| After Width: | Height: | Size: 3.7 KiB | 
| After Width: | Height: | Size: 4.3 KiB | 
| After Width: | Height: | Size: 6.5 KiB | 
| After Width: | Height: | Size: 6.0 KiB | 
| After Width: | Height: | Size: 4.7 KiB | 
| After Width: | Height: | Size: 5.8 KiB | 
| After Width: | Height: | Size: 4.4 KiB | 
| After Width: | Height: | Size: 3.9 KiB | 
| After Width: | Height: | Size: 6.8 KiB | 
| After Width: | Height: | Size: 3.3 KiB | 
| After Width: | Height: | Size: 6.6 KiB | 
| After Width: | Height: | Size: 7.6 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 5.6 KiB | 
| After Width: | Height: | Size: 4.7 KiB | 
| After Width: | Height: | Size: 5.4 KiB | 
| After Width: | Height: | Size: 6.3 KiB | 
| @ -0,0 +1,315 @@ | |||||||
|  | /*
 | ||||||
|  | https://github.com/peterix/dfhack
 | ||||||
|  | Copyright (c) 2011 Petr Mrázek <peterix@gmail.com> | ||||||
|  | 
 | ||||||
|  | A thread-safe logging console with a line editor for windows. | ||||||
|  | 
 | ||||||
|  | Based on linenoise win32 port, | ||||||
|  | copyright 2010, Jon Griffiths <jon_p_griffiths at yahoo dot com>. | ||||||
|  | All rights reserved. | ||||||
|  | Based on linenoise, copyright 2010, Salvatore Sanfilippo <antirez at gmail dot com>. | ||||||
|  | The original linenoise can be found at: http://github.com/antirez/linenoise
 | ||||||
|  | 
 | ||||||
|  | Redistribution and use in source and binary forms, with or without | ||||||
|  | modification, are permitted provided that the following conditions are met: | ||||||
|  | 
 | ||||||
|  |   * Redistributions of source code must retain the above copyright notice, | ||||||
|  |     this list of conditions and the following disclaimer. | ||||||
|  |   * Redistributions in binary form must reproduce the above copyright | ||||||
|  |     notice, this list of conditions and the following disclaimer in the | ||||||
|  |     documentation and/or other materials provided with the distribution. | ||||||
|  |   * Neither the name of Redis nor the names of its contributors may be used | ||||||
|  |     to endorse or promote products derived from this software without | ||||||
|  |     specific prior written permission. | ||||||
|  | 
 | ||||||
|  | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||||
|  | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||||
|  | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||||||
|  | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | ||||||
|  | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||||||
|  | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||||||
|  | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||||||
|  | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||||||
|  | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||||||
|  | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||||||
|  | POSSIBILITY OF SUCH DAMAGE. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #include <stdarg.h> | ||||||
|  | #include <errno.h> | ||||||
|  | #include <stdio.h> | ||||||
|  | #include <assert.h> | ||||||
|  | #include <iostream> | ||||||
|  | #include <fstream> | ||||||
|  | #include <istream> | ||||||
|  | #include <string> | ||||||
|  | #include <stdint.h> | ||||||
|  | 
 | ||||||
|  | #include <cstdio> | ||||||
|  | #include <cstdlib> | ||||||
|  | #include <sstream> | ||||||
|  | #include <vector> | ||||||
|  | 
 | ||||||
|  | #include <memory> | ||||||
|  | 
 | ||||||
|  | #include <md5wrapper.h> | ||||||
|  | 
 | ||||||
|  | using std::cout; | ||||||
|  | using std::cerr; | ||||||
|  | using std::endl; | ||||||
|  | 
 | ||||||
|  | typedef unsigned char patch_byte; | ||||||
|  | 
 | ||||||
|  | struct BinaryPatch { | ||||||
|  |     struct Byte { | ||||||
|  |         unsigned offset; | ||||||
|  |         patch_byte old_val, new_val; | ||||||
|  |     }; | ||||||
|  |     enum State { | ||||||
|  |         Conflict = 0, | ||||||
|  |         Unapplied = 1, | ||||||
|  |         Applied = 2, | ||||||
|  |         Partial = 3 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     std::vector<Byte> entries; | ||||||
|  | 
 | ||||||
|  |     bool loadDIF(std::string name); | ||||||
|  |     State checkState(const patch_byte *ptr, size_t len); | ||||||
|  | 
 | ||||||
|  |     void apply(patch_byte *ptr, size_t len, bool newv); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | inline bool is_hex(char c) | ||||||
|  | { | ||||||
|  |     return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool BinaryPatch::loadDIF(std::string name) | ||||||
|  | { | ||||||
|  |     entries.clear(); | ||||||
|  | 
 | ||||||
|  |     std::ifstream infile(name); | ||||||
|  |     if(infile.bad()) | ||||||
|  |     { | ||||||
|  |         cerr << "Cannot open file: " << name << endl; | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::string s; | ||||||
|  |     while(std::getline(infile, s)) | ||||||
|  |     { | ||||||
|  |         // Parse lines that begin with "[0-9a-f]+:"
 | ||||||
|  |         size_t idx = s.find(':'); | ||||||
|  |         if (idx == std::string::npos || idx == 0 || idx > 8) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         bool ok = true; | ||||||
|  |         for (size_t i = 0; i < idx; i++) | ||||||
|  |             if (!is_hex(s[i])) | ||||||
|  |                 ok = false; | ||||||
|  |         if (!ok) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         unsigned off, oval, nval; | ||||||
|  |         int nchar = 0; | ||||||
|  |         int cnt = sscanf(s.c_str(), "%x: %x %x%n", &off, &oval, &nval, &nchar); | ||||||
|  | 
 | ||||||
|  |         if (cnt < 3) | ||||||
|  |         { | ||||||
|  |             cerr << "Could not parse: " << s << endl; | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (size_t i = nchar; i < s.size(); i++) | ||||||
|  |         { | ||||||
|  |             if (!isspace(s[i])) | ||||||
|  |             { | ||||||
|  |                 cerr << "Garbage at end of line: " << s << endl; | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (oval >= 256 || nval >= 256) | ||||||
|  |         { | ||||||
|  |             cerr << "Invalid byte values: " << s << endl; | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Byte bv = { off, patch_byte(oval), patch_byte(nval) }; | ||||||
|  |         entries.push_back(bv); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (entries.empty()) | ||||||
|  |     { | ||||||
|  |         cerr << "No lines recognized." << endl; | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | BinaryPatch::State BinaryPatch::checkState(const patch_byte *ptr, size_t len) | ||||||
|  | { | ||||||
|  |     int state = 0; | ||||||
|  | 
 | ||||||
|  |     for (size_t i = 0; i < entries.size(); i++) | ||||||
|  |     { | ||||||
|  |         Byte &bv = entries[i]; | ||||||
|  | 
 | ||||||
|  |         if (bv.offset >= len) | ||||||
|  |         { | ||||||
|  |             cerr << "Offset out of range: 0x" << std::hex << bv.offset << std::dec << endl; | ||||||
|  |             return Conflict; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         patch_byte cv = ptr[bv.offset]; | ||||||
|  |         if (bv.old_val == cv) | ||||||
|  |             state |= Unapplied; | ||||||
|  |         else if (bv.new_val == cv) | ||||||
|  |             state |= Applied; | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             cerr << std::hex << bv.offset << ": " | ||||||
|  |                  << unsigned(bv.old_val) << " " << unsigned(bv.new_val) | ||||||
|  |                  << ", but currently " << unsigned(cv) << std::dec << endl; | ||||||
|  |             return Conflict; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return State(state); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void BinaryPatch::apply(patch_byte *ptr, size_t len, bool newv) | ||||||
|  | { | ||||||
|  |     for (size_t i = 0; i < entries.size(); i++) | ||||||
|  |     { | ||||||
|  |         Byte &bv = entries[i]; | ||||||
|  |         assert (bv.offset < len); | ||||||
|  | 
 | ||||||
|  |         ptr[bv.offset] = (newv ? bv.new_val : bv.old_val); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool load_file(std::vector<patch_byte> *pvec, std::string fname) | ||||||
|  | { | ||||||
|  |     FILE *f = fopen(fname.c_str(), "rb"); | ||||||
|  |     if (!f) | ||||||
|  |     { | ||||||
|  |         cerr << "Cannot open file: " << fname << endl; | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fseek(f, 0, SEEK_END); | ||||||
|  |     pvec->resize(ftell(f)); | ||||||
|  |     fseek(f, 0, SEEK_SET); | ||||||
|  |     size_t cnt = fread(pvec->data(), 1, pvec->size(), f); | ||||||
|  |     fclose(f); | ||||||
|  | 
 | ||||||
|  |     return cnt == pvec->size(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool save_file(const std::vector<patch_byte> &pvec, std::string fname) | ||||||
|  | { | ||||||
|  |     FILE *f = fopen(fname.c_str(), "wb"); | ||||||
|  |     if (!f) | ||||||
|  |     { | ||||||
|  |         cerr << "Cannot open file: " << fname << endl; | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     size_t cnt = fwrite(pvec.data(), 1, pvec.size(), f); | ||||||
|  |     fclose(f); | ||||||
|  | 
 | ||||||
|  |     return cnt == pvec.size(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string compute_hash(const std::vector<patch_byte> &pvec) | ||||||
|  | { | ||||||
|  |     md5wrapper md5; | ||||||
|  |     return md5.getHashFromBytes(pvec.data(), pvec.size()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | int main (int argc, char *argv[]) | ||||||
|  | { | ||||||
|  |     if (argc <= 3) | ||||||
|  |     { | ||||||
|  |         cerr << "Usage: binpatch check|apply|remove <exe> <patch>" << endl; | ||||||
|  |         return 2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::string cmd = argv[1]; | ||||||
|  | 
 | ||||||
|  |     if (cmd != "check" && cmd != "apply" && cmd != "remove") | ||||||
|  |     { | ||||||
|  |         cerr << "Invalid command: " << cmd << endl; | ||||||
|  |         return 2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::string exe_file = argv[2]; | ||||||
|  |     std::vector<patch_byte> bindata; | ||||||
|  |     if (!load_file(&bindata, exe_file)) | ||||||
|  |         return 2; | ||||||
|  | 
 | ||||||
|  |     BinaryPatch patch; | ||||||
|  |     if (!patch.loadDIF(argv[3])) | ||||||
|  |         return 2; | ||||||
|  | 
 | ||||||
|  |     BinaryPatch::State state = patch.checkState(bindata.data(), bindata.size()); | ||||||
|  |     if (state == BinaryPatch::Conflict) | ||||||
|  |         return 1; | ||||||
|  | 
 | ||||||
|  |     if (cmd == "check") | ||||||
|  |     { | ||||||
|  |         switch (state) | ||||||
|  |         { | ||||||
|  |         case BinaryPatch::Unapplied: | ||||||
|  |             cout << "Currently not applied." << endl; | ||||||
|  |             break; | ||||||
|  |         case BinaryPatch::Applied: | ||||||
|  |             cout << "Currently applied." << endl; | ||||||
|  |             break; | ||||||
|  |         case BinaryPatch::Partial: | ||||||
|  |             cout << "Currently partially applied." << endl; | ||||||
|  |             break; | ||||||
|  |         default: | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  |     else if (cmd == "apply") | ||||||
|  |     { | ||||||
|  |         if (state == BinaryPatch::Applied) | ||||||
|  |         { | ||||||
|  |             cout << "Already applied." << endl; | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         patch.apply(bindata.data(), bindata.size(), true); | ||||||
|  |     } | ||||||
|  |     else if (cmd == "remove") | ||||||
|  |     { | ||||||
|  |         if (state == BinaryPatch::Unapplied) | ||||||
|  |         { | ||||||
|  |             cout << "Already removed." << endl; | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         patch.apply(bindata.data(), bindata.size(), false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!save_file(bindata, exe_file + ".bak")) | ||||||
|  |     { | ||||||
|  |         cerr << "Could not create backup." << endl; | ||||||
|  |         return 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!save_file(bindata, exe_file)) | ||||||
|  |         return 1; | ||||||
|  | 
 | ||||||
|  |     cout << "Patched " << patch.entries.size() | ||||||
|  |          << " bytes, new hash: " << compute_hash(bindata) << endl; | ||||||
|  |     return 0; | ||||||
|  | } | ||||||
| @ -0,0 +1,60 @@ | |||||||
|  | #pragma once | ||||||
|  | #ifndef EVENT_MANAGER_H_INCLUDED | ||||||
|  | #define EVENT_MANAGER_H_INCLUDED | ||||||
|  | 
 | ||||||
|  | #include "Core.h" | ||||||
|  | #include "Export.h" | ||||||
|  | #include "ColorText.h" | ||||||
|  | #include "PluginManager.h" | ||||||
|  | #include "Console.h" | ||||||
|  | 
 | ||||||
|  | namespace DFHack { | ||||||
|  |     namespace EventManager { | ||||||
|  |         namespace EventType { | ||||||
|  |             enum EventType { | ||||||
|  |                 TICK, | ||||||
|  |                 JOB_INITIATED, | ||||||
|  |                 JOB_COMPLETED, | ||||||
|  |                 UNIT_DEATH, | ||||||
|  |                 ITEM_CREATED, | ||||||
|  |                 BUILDING, | ||||||
|  |                 CONSTRUCTION, | ||||||
|  |                 SYNDROME, | ||||||
|  |                 INVASION, | ||||||
|  |                 EVENT_MAX | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         struct EventHandler { | ||||||
|  |             void (*eventHandler)(color_ostream&, void*); //called when the event happens
 | ||||||
|  |             int32_t freq; | ||||||
|  | 
 | ||||||
|  |             EventHandler(void (*eventHandlerIn)(color_ostream&, void*), int32_t freqIn): eventHandler(eventHandlerIn), freq(freqIn) { | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             bool operator==(EventHandler& handle) const { | ||||||
|  |                 return eventHandler == handle.eventHandler && freq == handle.freq; | ||||||
|  |             } | ||||||
|  |             bool operator!=(EventHandler& handle) const { | ||||||
|  |                 return !( *this == handle); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         struct SyndromeData { | ||||||
|  |             int32_t unitId; | ||||||
|  |             int32_t syndromeIndex; | ||||||
|  |             SyndromeData(int32_t unitId_in, int32_t syndromeIndex_in): unitId(unitId_in), syndromeIndex(syndromeIndex_in) { | ||||||
|  | 
 | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         DFHACK_EXPORT void registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin); | ||||||
|  |         DFHACK_EXPORT void registerTick(EventHandler handler, int32_t when, Plugin* plugin, bool absolute=false); | ||||||
|  |         DFHACK_EXPORT void unregister(EventType::EventType e, EventHandler handler, Plugin* plugin); | ||||||
|  |         DFHACK_EXPORT void unregisterAll(Plugin* plugin); | ||||||
|  |         void manageEvents(color_ostream& out); | ||||||
|  |         void onStateChange(color_ostream& out, state_change_event event); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #endif | ||||||
| @ -1,70 +0,0 @@ | |||||||
| /*
 |  | ||||||
| https://github.com/peterix/dfhack
 |  | ||||||
| Copyright (c) 2009-2012 Petr Mrázek (peterix@gmail.com) |  | ||||||
| 
 |  | ||||||
| This software is provided 'as-is', without any express or implied |  | ||||||
| warranty. In no event will the authors be held liable for any |  | ||||||
| damages arising from the use of this software. |  | ||||||
| 
 |  | ||||||
| Permission is granted to anyone to use this software for any |  | ||||||
| purpose, including commercial applications, and to alter it and |  | ||||||
| redistribute it freely, subject to the following restrictions: |  | ||||||
| 
 |  | ||||||
| 1. The origin of this software must not be misrepresented; you must |  | ||||||
| not claim that you wrote the original software. If you use this |  | ||||||
| software in a product, an acknowledgment in the product documentation |  | ||||||
| would be appreciated but is not required. |  | ||||||
| 
 |  | ||||||
| 2. Altered source versions must be plainly marked as such, and |  | ||||||
| must not be misrepresented as being the original software. |  | ||||||
| 
 |  | ||||||
| 3. This notice may not be removed or altered from any source |  | ||||||
| distribution. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| #pragma once |  | ||||||
| #ifndef CL_MOD_VEGETATION |  | ||||||
| #define CL_MOD_VEGETATION |  | ||||||
| /**
 |  | ||||||
|  * \defgroup grp_vegetation Vegetation : stuff that grows and gets cut down or trampled by dwarves |  | ||||||
|  * @ingroup grp_modules |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| #include "Export.h" |  | ||||||
| #include "DataDefs.h" |  | ||||||
| #include "df/plant.h" |  | ||||||
| 
 |  | ||||||
| namespace DFHack |  | ||||||
| { |  | ||||||
| namespace Vegetation |  | ||||||
| { |  | ||||||
| const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3; // 3 years
 |  | ||||||
| 
 |  | ||||||
| // "Simplified" copy of plant
 |  | ||||||
| struct t_plant { |  | ||||||
|     df::language_name name; |  | ||||||
|     df::plant_flags flags; |  | ||||||
|     int16_t material; |  | ||||||
|     df::coord pos; |  | ||||||
|     int32_t grow_counter; |  | ||||||
|     uint16_t temperature_1; |  | ||||||
|     uint16_t temperature_2; |  | ||||||
|     int32_t is_burning; |  | ||||||
|     int32_t hitpoints; |  | ||||||
|     int16_t update_order; |  | ||||||
|     //std::vector<void *> unk1;
 |  | ||||||
|     //int32_t unk2;
 |  | ||||||
|     //uint16_t temperature_3;
 |  | ||||||
|     //uint16_t temperature_4;
 |  | ||||||
|     //uint16_t temperature_5;
 |  | ||||||
|     // Pointer to original object, in case you want to modify it
 |  | ||||||
|     df::plant *origin; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| DFHACK_EXPORT bool isValid(); |  | ||||||
| DFHACK_EXPORT uint32_t getCount(); |  | ||||||
| DFHACK_EXPORT df::plant * getPlant(const int32_t index); |  | ||||||
| DFHACK_EXPORT bool copyPlant (const int32_t index, t_plant &out); |  | ||||||
| } |  | ||||||
| } |  | ||||||
| #endif |  | ||||||
| @ -0,0 +1,123 @@ | |||||||
|  | -- Simple binary patch with IDA dif file support. | ||||||
|  | 
 | ||||||
|  | local _ENV = mkmodule('binpatch') | ||||||
|  | 
 | ||||||
|  | local function load_patch(name) | ||||||
|  |     local filename = name | ||||||
|  |     if not string.match(filename, '[./\\]') then | ||||||
|  |         filename = dfhack.getHackPath()..'/patches/'..dfhack.getDFVersion()..'/'..name..'.dif' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     local file, err = io.open(filename, 'r') | ||||||
|  |     if not file then | ||||||
|  |         if string.match(err, ': No such file or directory') then | ||||||
|  |             return nil, 'patch not found' | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     local old_bytes = {} | ||||||
|  |     local new_bytes = {} | ||||||
|  | 
 | ||||||
|  |     for line in file:lines() do | ||||||
|  |         if string.match(line, '^%x+:') then | ||||||
|  |             local offset, oldv, newv = string.match(line, '^(%x+):%s*(%x+)%s+(%x+)%s*$') | ||||||
|  |             if not offset then | ||||||
|  |                 file:close() | ||||||
|  |                 return nil, 'could not parse: '..line | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             offset, oldv, newv = tonumber(offset,16), tonumber(oldv,16), tonumber(newv,16) | ||||||
|  |             if oldv > 255 or newv > 255 then | ||||||
|  |                 file:close() | ||||||
|  |                 return nil, 'invalid byte values: '..line | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             old_bytes[offset] = oldv | ||||||
|  |             new_bytes[offset] = newv | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     return { name = name, old_bytes = old_bytes, new_bytes = new_bytes } | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function rebase_table(input) | ||||||
|  |     local output = {} | ||||||
|  |     local base = dfhack.internal.getImageBase() | ||||||
|  |     for k,v in pairs(input) do | ||||||
|  |         local offset = dfhack.internal.adjustOffset(k) | ||||||
|  |         if not offset then | ||||||
|  |             return nil, string.format('invalid offset: %x', k) | ||||||
|  |         end | ||||||
|  |         output[base + offset] = v | ||||||
|  |     end | ||||||
|  |     return output | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function rebase_patch(patch) | ||||||
|  |     local nold, err = rebase_table(patch.old_bytes) | ||||||
|  |     if not nold then return nil, err end | ||||||
|  |     local nnew, err = rebase_table(patch.new_bytes) | ||||||
|  |     if not nnew then return nil, err end | ||||||
|  |     return { name = patch.name, old_bytes = nold, new_bytes = nnew } | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | BinaryPatch = defclass(BinaryPatch) | ||||||
|  | 
 | ||||||
|  | BinaryPatch.ATTRS { | ||||||
|  |     name = DEFAULT_NIL, | ||||||
|  |     old_bytes = DEFAULT_NIL, | ||||||
|  |     new_bytes = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function load_dif_file(name) | ||||||
|  |     local patch, err = load_patch(name) | ||||||
|  |     if not patch then return nil, err end | ||||||
|  | 
 | ||||||
|  |     local rpatch, err = rebase_patch(patch) | ||||||
|  |     if not rpatch then return nil, err end | ||||||
|  | 
 | ||||||
|  |     return BinaryPatch(rpatch) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BinaryPatch:status() | ||||||
|  |     local old_ok, err, addr = dfhack.internal.patchBytes({}, self.old_bytes) | ||||||
|  |     if old_ok then | ||||||
|  |         return 'removed' | ||||||
|  |     elseif dfhack.internal.patchBytes({}, self.new_bytes) then | ||||||
|  |         return 'applied' | ||||||
|  |     else | ||||||
|  |         return 'conflict', addr | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BinaryPatch:isApplied() | ||||||
|  |     return dfhack.internal.patchBytes({}, self.new_bytes) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BinaryPatch:apply() | ||||||
|  |     local ok, err, addr = dfhack.internal.patchBytes(self.new_bytes, self.old_bytes) | ||||||
|  |     if ok then | ||||||
|  |         return true, 'applied the patch' | ||||||
|  |     elseif dfhack.internal.patchBytes({}, self.new_bytes) then | ||||||
|  |         return true, 'patch is already applied' | ||||||
|  |     else | ||||||
|  |         return false, string.format('conflict at address %x', addr) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BinaryPatch:isRemoved() | ||||||
|  |     return dfhack.internal.patchBytes({}, self.old_bytes) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BinaryPatch:remove() | ||||||
|  |     local ok, err, addr = dfhack.internal.patchBytes(self.old_bytes, self.new_bytes) | ||||||
|  |     if ok then | ||||||
|  |         return true, 'removed the patch' | ||||||
|  |     elseif dfhack.internal.patchBytes({}, self.old_bytes) then | ||||||
|  |         return true, 'patch is already removed' | ||||||
|  |     else | ||||||
|  |         return false, string.format('conflict at address %x', addr) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | return _ENV | ||||||
| @ -0,0 +1,591 @@ | |||||||
|  | local _ENV = mkmodule('dfhack.workshops') | ||||||
|  | 
 | ||||||
|  | local utils = require 'utils' | ||||||
|  | 
 | ||||||
|  | input_filter_defaults = { | ||||||
|  |     item_type = -1, | ||||||
|  |     item_subtype = -1, | ||||||
|  |     mat_type = -1, | ||||||
|  |     mat_index = -1, | ||||||
|  |     flags1 = {}, | ||||||
|  |     -- Instead of noting those that allow artifacts, mark those that forbid them. | ||||||
|  |     -- Leaves actually enabling artifacts to the discretion of the API user, | ||||||
|  |     -- which is the right thing because unlike the game UI these filters are | ||||||
|  |     -- used in a way that does not give the user a chance to choose manually. | ||||||
|  |     flags2 = { allow_artifact = true }, | ||||||
|  |     flags3 = {}, | ||||||
|  |     flags4 = 0, | ||||||
|  |     flags5 = 0, | ||||||
|  |     reaction_class = '', | ||||||
|  |     has_material_reaction_product = '', | ||||||
|  |     metal_ore = -1, | ||||||
|  |     min_dimension = -1, | ||||||
|  |     has_tool_use = -1, | ||||||
|  |     quantity = 1 | ||||||
|  | } | ||||||
|  | local fuel={item_type=df.item_type.BAR,mat_type=df.builtin_mats.COAL} | ||||||
|  | jobs_furnace={ | ||||||
|  |     [df.furnace_type.Smelter]={ | ||||||
|  |         { | ||||||
|  |             name="Melt metal object", | ||||||
|  |             items={fuel,{flags2={allow_melt_dump=true}}},--also maybe melt_designated | ||||||
|  |             job_fields={job_type=df.job_type.MeltMetalObject} | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     [df.furnace_type.MagmaSmelter]={ | ||||||
|  |         { | ||||||
|  |             name="Melt metal object", | ||||||
|  |             items={{flags2={allow_melt_dump=true}}},--also maybe melt_designated | ||||||
|  |             job_fields={job_type=df.job_type.MeltMetalObject} | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     --[[ [df.furnace_type.MetalsmithsForge]={ | ||||||
|  |         unpack(concat(furnaces,mechanism,anvil,crafts,coins,flask)) | ||||||
|  |          | ||||||
|  |     }, | ||||||
|  |     ]] | ||||||
|  |     --MetalsmithsForge, | ||||||
|  |     --MagmaForge | ||||||
|  |     --[[ | ||||||
|  |         forges: | ||||||
|  |             weapons and ammo-> from raws... | ||||||
|  |             armor -> raws | ||||||
|  |             furniture -> builtins? | ||||||
|  |             siege eq-> builtin (only balista head) | ||||||
|  |             trap eq -> from raws+ mechanisms | ||||||
|  |             other object-> anvil, crafts, goblets,toys,instruments,nestbox... (raws?) flask, coins,stud with iron | ||||||
|  |             metal clothing-> raws??? | ||||||
|  |     ]] | ||||||
|  |     [df.furnace_type.GlassFurnace]={ | ||||||
|  |         { | ||||||
|  |             name="collect sand", | ||||||
|  |             items={}, | ||||||
|  |             job_fields={job_type=df.job_type.CollectSand} | ||||||
|  |         }, | ||||||
|  |         --glass crafts x3 | ||||||
|  |     }, | ||||||
|  |     [df.furnace_type.WoodFurnace]={ | ||||||
|  |         defaults={item_type=df.item_type.WOOD,vector_id=df.job_item_vector_id.WOOD}, | ||||||
|  |         { | ||||||
|  |             name="make charcoal", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeCharcoal} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="make ash", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeAsh} | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     [df.furnace_type.Kiln]={ | ||||||
|  |         { | ||||||
|  |             name="collect clay", | ||||||
|  |             items={}, | ||||||
|  |             job_fields={job_type=df.job_type.CollectClay} | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  | jobs_workshop={ | ||||||
|  |      | ||||||
|  |     [df.workshop_type.Jewelers]={ | ||||||
|  |         { | ||||||
|  |             name="cut gems", | ||||||
|  |             items={{item_type=df.item_type.ROUGH,flags1={unrotten=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.CutGems} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="encrust finished goods with gems", | ||||||
|  |             items={{item_type=df.item_type.SMALLGEM},{flags1={improvable=true,finished_goods=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.EncrustWithGems} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="encrust ammo with gems", | ||||||
|  |             items={{item_type=df.item_type.SMALLGEM},{flags1={improvable=true,ammo=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.EncrustWithGems} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="encrust furniture with gems", | ||||||
|  |             items={{item_type=df.item_type.SMALLGEM},{flags1={improvable=true,furniture=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.EncrustWithGems} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Fishery]={ | ||||||
|  |         { | ||||||
|  |             name="prepare raw fish", | ||||||
|  |             items={{item_type=df.item_type.FISH_RAW,flags1={unrotten=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.PrepareRawFish} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="extract from raw fish", | ||||||
|  |             items={{flags1={unrotten=true,extract_bearing_fish=true}},{item_type=df.item_type.FLASK,flags1={empty=true,glass=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.ExtractFromRawFish} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="catch live fish", | ||||||
|  |             items={}, | ||||||
|  |             job_fields={job_type=df.job_type.CatchLiveFish} | ||||||
|  |         }, -- no items? | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Still]={ | ||||||
|  |         { | ||||||
|  |             name="brew drink", | ||||||
|  |             items={{flags1={distillable=true},vector_id=22},{flags1={empty=true},flags3={food_storage=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.BrewDrink} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="extract from plants", | ||||||
|  |             items={{item_type=df.item_type.PLANT,flags1={unrotten=true,extract_bearing_plant=true}},{item_type=df.item_type.FLASK,flags1={empty=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.ExtractFromPlants} | ||||||
|  |         }, | ||||||
|  |         --mead from raws? | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Masons]={ | ||||||
|  |         defaults={item_type=df.item_type.BOULDER,item_subtype=-1,vector_id=df.job_item_vector_id.BOULDER, mat_type=0,mat_index=-1,flags3={hard=true}},--flags2={non_economic=true}, | ||||||
|  |         { | ||||||
|  |             name="construct armor stand", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructArmorStand} | ||||||
|  |             }, | ||||||
|  |              | ||||||
|  |         { | ||||||
|  |             name="construct blocks", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructBlocks} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct throne", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructThrone} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct coffin", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructCoffin} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct door", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructDoor} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct floodgate", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructFloodgate} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct hatch cover", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructHatchCover} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct grate", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructGrate} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct cabinet", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructCabinet} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct chest", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructChest} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct statue", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructStatue} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct slab", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructSlab} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct table", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructTable} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct weapon rack", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructWeaponRack} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct quern", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructQuern} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct millstone", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructMillstone} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Carpenters]={ | ||||||
|  |         --training weapons, wooden shields | ||||||
|  |         defaults={item_type=df.item_type.WOOD,vector_id=df.job_item_vector_id.WOOD}, | ||||||
|  |          | ||||||
|  |         { | ||||||
|  |             name="make barrel", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeBarrel} | ||||||
|  |         }, | ||||||
|  |          | ||||||
|  |         { | ||||||
|  |             name="make bucket", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeBucket} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="make animal trap", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeAnimalTrap} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="make cage", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeCage} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct bed", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructBed} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct bin", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructBin} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct armor stand", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructArmorStand} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct blocks", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructBlocks} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct throne", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructThrone} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct coffin", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructCoffin} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct door", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructDoor} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct floodgate", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructFloodgate} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct hatch cover", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructHatchCover} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct grate", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructGrate} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct cabinet", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructCabinet} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct chest", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructChest} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct statue", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructStatue} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct table", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructTable} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct weapon rack", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructWeaponRack} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct splint", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructSplint} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct crutch", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructCrutch} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Kitchen]={ | ||||||
|  |         --mat_type=2,3,4 | ||||||
|  |         defaults={flags1={unrotten=true,cookable=true}}, | ||||||
|  |         { | ||||||
|  |             name="prepare easy meal", | ||||||
|  |             items={{flags1={solid=true}},{}}, | ||||||
|  |             job_fields={job_type=df.job_type.PrepareMeal,mat_type=2} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="prepare fine meal", | ||||||
|  |             items={{flags1={solid=true}},{},{}}, | ||||||
|  |             job_fields={job_type=df.job_type.PrepareMeal,mat_type=3} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="prepare lavish meal", | ||||||
|  |             items={{flags1={solid=true}},{},{},{}}, | ||||||
|  |             job_fields={job_type=df.job_type.PrepareMeal,mat_type=4} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Butchers]={ | ||||||
|  |         { | ||||||
|  |             name="butcher an animal", | ||||||
|  |             items={{flags1={butcherable=true,unrotten=true,nearby=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.ButcherAnimal} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="extract from land animal", | ||||||
|  |             items={{flags1={extract_bearing_vermin=true,unrotten=true}},{item_type=df.item_type.FLASK,flags1={empty=true,glass=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.ExtractFromLandAnimal} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="catch live land animal", | ||||||
|  |             items={}, | ||||||
|  |             job_fields={job_type=df.job_type.CatchLiveLandAnimal} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Mechanics]={ | ||||||
|  |         { | ||||||
|  |             name="construct mechanisms", | ||||||
|  |             items={{item_type=df.item_type.BOULDER,item_subtype=-1,vector_id=df.job_item_vector_id.BOULDER, mat_type=0,mat_index=-1,quantity=1, | ||||||
|  |                 flags3={hard=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructMechanisms} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct traction bench", | ||||||
|  |             items={{item_type=df.item_type.TABLE},{item_type=df.item_type.MECHANISM},{item_type=df.item_type.CHAIN}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructTractionBench} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Loom]={ | ||||||
|  |         { | ||||||
|  |             name="weave plant thread cloth", | ||||||
|  |             items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={plant=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.WeaveCloth} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="weave silk thread cloth", | ||||||
|  |             items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={silk=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.WeaveCloth} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="weave yarn cloth", | ||||||
|  |             items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={yarn=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.WeaveCloth} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="weave inorganic cloth", | ||||||
|  |             items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},mat_type=0}}, | ||||||
|  |             job_fields={job_type=df.job_type.WeaveCloth} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="collect webs", | ||||||
|  |             items={{item_type=df.item_type.THREAD,quantity=10,min_dimension=10,flags1={undisturbed=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.CollectWebs} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Leatherworks]={ | ||||||
|  |         defaults={item_type=SKIN_TANNED}, | ||||||
|  |         { | ||||||
|  |             name="construct leather bag", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructChest} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct waterskin", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeFlask} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct backpack", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeBackpack} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct quiver", | ||||||
|  |             items={{}}, | ||||||
|  |             job_fields={job_type=df.job_type.MakeQuiver} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="sew leather image", | ||||||
|  |             items={{item_type=-1,flags1={empty=true},flags2={sewn_imageless=true}},{}}, | ||||||
|  |             job_fields={job_type=df.job_type.SewImage} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Dyers]={ | ||||||
|  |         { | ||||||
|  |             name="dye thread", | ||||||
|  |             items={{item_type=df.item_type.THREAD,quantity=15000,min_dimension=15000,flags1={collected=true},flags2={dyeable=true}}, | ||||||
|  |                 {flags1={unrotten=true},flags2={dye=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.DyeThread} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="dye cloth", | ||||||
|  |             items={{item_type=df.item_type.CLOTH,quantity=10000,min_dimension=10000,flags2={dyeable=true}}, | ||||||
|  |                 {flags1={unrotten=true},flags2={dye=true}}}, | ||||||
|  |             job_fields={job_type=df.job_type.DyeThread} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     [df.workshop_type.Siege]={ | ||||||
|  |         { | ||||||
|  |             name="construct balista parts", | ||||||
|  |             items={{item_type=df.item_type.WOOD}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructBallistaParts} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="construct catapult parts", | ||||||
|  |             items={{item_type=df.item_type.WOOD}}, | ||||||
|  |             job_fields={job_type=df.job_type.ConstructCatapultParts} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="assemble balista arrow", | ||||||
|  |             items={{item_type=df.item_type.WOOD}}, | ||||||
|  |             job_fields={job_type=df.job_type.AssembleSiegeAmmo} | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name="assemble tipped balista arrow", | ||||||
|  |             items={{item_type=df.item_type.WOOD},{item_type=df.item_type.BALLISTAARROWHEAD}}, | ||||||
|  |             job_fields={job_type=df.job_type.AssembleSiegeAmmo} | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  | local function matchIds(bid1,wid1,cid1,bid2,wid2,cid2) | ||||||
|  |     if bid1~=-1 and bid2~=-1 and bid1~=bid2 then | ||||||
|  |         return false | ||||||
|  |     end | ||||||
|  |     if wid1~=-1 and wid2~=-1 and wid1~=wid2 then | ||||||
|  |         return false | ||||||
|  |     end | ||||||
|  |     if cid1~=-1 and cid2~=-1 and cid1~=cid2 then | ||||||
|  |         return false | ||||||
|  |     end | ||||||
|  |     return true | ||||||
|  | end | ||||||
|  | local function scanRawsReaction(buildingId,workshopId,customId) | ||||||
|  |     local ret={} | ||||||
|  |     for idx,reaction in ipairs(df.global.world.raws.reactions) do | ||||||
|  |         for k,v in pairs(reaction.building.type) do | ||||||
|  |             if matchIds(buildingId,workshopId,customId,v,reaction.building.subtype[k],reaction.building.custom[k]) then | ||||||
|  |                 table.insert(ret,reaction) | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     return ret | ||||||
|  | end | ||||||
|  | local function reagentToJobItem(reagent,react_id,reagentId) | ||||||
|  |     local ret_item | ||||||
|  |     ret_item=utils.clone_with_default(reagent, input_filter_defaults) | ||||||
|  |     ret_item.reaction_id=react_id | ||||||
|  |     ret_item.reagent_index=reagentId | ||||||
|  |     return ret_item | ||||||
|  | end | ||||||
|  | local function addReactionJobs(ret,bid,wid,cid) | ||||||
|  |     local reactions=scanRawsReaction(bid,wid or -1,cid or -1) | ||||||
|  |     for idx,react in pairs(reactions) do | ||||||
|  |     local job={name=react.name, | ||||||
|  |                items={},job_fields={job_type=df.job_type.CustomReaction,reaction_name=react.code} | ||||||
|  |               } | ||||||
|  |         for reagentId,reagent in pairs(react.reagents) do | ||||||
|  |             table.insert(job.items,reagentToJobItem(reagent,idx,reagentId)) | ||||||
|  |         end | ||||||
|  |         if react.flags.FUEL then | ||||||
|  |             table.insert(job.items,fuel) | ||||||
|  |         end | ||||||
|  |         table.insert(ret,job) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | local function scanRawsOres() | ||||||
|  |     local ret={} | ||||||
|  |     for idx,ore in ipairs(df.global.world.raws.inorganics) do | ||||||
|  |         if #ore.metal_ore.mat_index~=0 then | ||||||
|  |             ret[idx]=ore | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     return ret | ||||||
|  | end | ||||||
|  | local function addSmeltJobs(ret,use_fuel) | ||||||
|  |     local ores=scanRawsOres() | ||||||
|  |     for idx,ore in pairs(ores) do | ||||||
|  |         print("adding:",ore.material.state_name.Solid) | ||||||
|  |         printall(ore) | ||||||
|  |     local job={name="smelt "..ore.material.state_name.Solid,job_fields={job_type=df.job_type.SmeltOre,mat_type=df.builtin_mats.INORGANIC,mat_index=idx},items={ | ||||||
|  |         {item_type=df.item_type.BOULDER,mat_type=df.builtin_mats.INORGANIC,mat_index=idx,vector_id=df.job_item_vector_id.BOULDER}}} | ||||||
|  |         if use_fuel then | ||||||
|  |             table.insert(job.items,fuel) | ||||||
|  |         end | ||||||
|  |         table.insert(ret,job) | ||||||
|  |     end | ||||||
|  |     return ret | ||||||
|  | end | ||||||
|  | function getJobs(buildingId,workshopId,customId) | ||||||
|  |     local ret={} | ||||||
|  |     local c_jobs | ||||||
|  |     if buildingId==df.building_type.Workshop then | ||||||
|  |         c_jobs=jobs_workshop[workshopId] | ||||||
|  |     elseif buildingId==df.building_type.Furnace then | ||||||
|  |         c_jobs=jobs_furnace[workshopId] | ||||||
|  |          | ||||||
|  |         if workshopId == df.furnace_type.Smelter or workshopId == df.furnace_type.MagmaSmelter then | ||||||
|  |             c_jobs=utils.clone(c_jobs,true) | ||||||
|  |             addSmeltJobs(c_jobs,workshopId == df.furnace_type.Smelter) | ||||||
|  |         end | ||||||
|  |     else | ||||||
|  |         return nil | ||||||
|  |     end | ||||||
|  |     if c_jobs==nil then | ||||||
|  |         c_jobs={} | ||||||
|  |     else | ||||||
|  |         c_jobs=utils.clone(c_jobs,true) | ||||||
|  |     end | ||||||
|  |      | ||||||
|  |     addReactionJobs(c_jobs,buildingId,workshopId,customId) | ||||||
|  |     for jobId,contents in pairs(c_jobs) do | ||||||
|  |         if jobId~="defaults" then | ||||||
|  |             local entry={} | ||||||
|  |             entry.name=contents.name | ||||||
|  |             local lclDefaults=utils.clone(input_filter_defaults,true) | ||||||
|  |             if c_jobs.defaults ~=nil then | ||||||
|  |                     utils.assign(lclDefaults,c_jobs.defaults) | ||||||
|  |             end | ||||||
|  |             entry.items={} | ||||||
|  |             for k,item in pairs(contents.items) do | ||||||
|  |                 entry.items[k]=utils.clone(lclDefaults,true) | ||||||
|  |                 utils.assign(entry.items[k],item) | ||||||
|  |             end | ||||||
|  |             if contents.job_fields~=nil then | ||||||
|  |                 entry.job_fields={} | ||||||
|  |                 utils.assign(entry.job_fields,contents.job_fields) | ||||||
|  |             end | ||||||
|  |             ret[jobId]=entry | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     --get jobs, add in from raws | ||||||
|  |     return ret | ||||||
|  | end | ||||||
|  | return _ENV | ||||||
| @ -0,0 +1,285 @@ | |||||||
|  | -- Stock dialog for selecting buildings | ||||||
|  | 
 | ||||||
|  | local _ENV = mkmodule('gui.buildings') | ||||||
|  | 
 | ||||||
|  | local gui = require('gui') | ||||||
|  | local widgets = require('gui.widgets') | ||||||
|  | local dlg = require('gui.dialogs') | ||||||
|  | local utils = require('utils') | ||||||
|  | 
 | ||||||
|  | ARROW = string.char(26) | ||||||
|  | 
 | ||||||
|  | WORKSHOP_ABSTRACT={ | ||||||
|  |     [df.building_type.Civzone]=true,[df.building_type.Stockpile]=true, | ||||||
|  | } | ||||||
|  | WORKSHOP_SPECIAL={ | ||||||
|  |     [df.building_type.Workshop]=true,[df.building_type.Furnace]=true,[df.building_type.Trap]=true, | ||||||
|  |     [df.building_type.Construction]=true,[df.building_type.SiegeEngine]=true | ||||||
|  | } | ||||||
|  | BuildingDialog = defclass(BuildingDialog, gui.FramedScreen) | ||||||
|  | 
 | ||||||
|  | BuildingDialog.focus_path = 'BuildingDialog' | ||||||
|  | 
 | ||||||
|  | BuildingDialog.ATTRS{ | ||||||
|  |     prompt = 'Type or select a building from this list', | ||||||
|  |     frame_style = gui.GREY_LINE_FRAME, | ||||||
|  |     frame_inset = 1, | ||||||
|  |     frame_title = 'Select Building', | ||||||
|  |     -- new attrs | ||||||
|  |     none_caption = 'none', | ||||||
|  |     hide_none = false, | ||||||
|  |     use_abstract = true, | ||||||
|  |     use_workshops = true, | ||||||
|  |     use_tool_workshop=true, | ||||||
|  |     use_furnace = true, | ||||||
|  |     use_construction = true, | ||||||
|  |     use_siege = true, | ||||||
|  |     use_trap = true, | ||||||
|  |     use_custom = true, | ||||||
|  |     building_filter = DEFAULT_NIL, | ||||||
|  |     on_select = DEFAULT_NIL, | ||||||
|  |     on_cancel = DEFAULT_NIL, | ||||||
|  |     on_close = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:init(info) | ||||||
|  |     self:addviews{ | ||||||
|  |         widgets.Label{ | ||||||
|  |             text = { | ||||||
|  |                 self.prompt, '\n\n', | ||||||
|  |                 'Category: ', { text = self:cb_getfield('context_str'), pen = COLOR_CYAN } | ||||||
|  |             }, | ||||||
|  |             text_pen = COLOR_WHITE, | ||||||
|  |             frame = { l = 0, t = 0 }, | ||||||
|  |         }, | ||||||
|  |         widgets.Label{ | ||||||
|  |             view_id = 'back', | ||||||
|  |             visible = false, | ||||||
|  |             text = { { key = 'LEAVESCREEN', text = ': Back' } }, | ||||||
|  |             frame = { r = 0, b = 0 }, | ||||||
|  |             auto_width = true, | ||||||
|  |         }, | ||||||
|  |         widgets.FilteredList{ | ||||||
|  |             view_id = 'list', | ||||||
|  |             not_found_label = 'No matching buildings', | ||||||
|  |             frame = { l = 0, r = 0, t = 4, b = 2 }, | ||||||
|  |             icon_width = 2, | ||||||
|  |             on_submit = self:callback('onSubmitItem'), | ||||||
|  |         }, | ||||||
|  |         widgets.Label{ | ||||||
|  |             text = { { | ||||||
|  |                 key = 'SELECT', text = ': Select', | ||||||
|  |                 disabled = function() return not self.subviews.list:canSubmit() end | ||||||
|  |             } }, | ||||||
|  |             frame = { l = 0, b = 0 }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     self:initBuiltinMode() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:getWantedFrameSize(rect) | ||||||
|  |     return math.max(self.frame_width or 40, #self.prompt), math.min(28, rect.height-8) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:onDestroy() | ||||||
|  |     if self.on_close then | ||||||
|  |         self.on_close() | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:initBuiltinMode() | ||||||
|  |     local choices = {} | ||||||
|  |     if not self.hide_none then | ||||||
|  |         table.insert(choices, { text = self.none_caption, type_id = -1, subtype_id = -1, custom_id=-1}) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     if self.use_workshops then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'workshop', key = 'CUSTOM_SHIFT_W', | ||||||
|  |             cb = self:callback('initWorkshopMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_furnace then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'furnaces', key = 'CUSTOM_SHIFT_F', | ||||||
|  |             cb = self:callback('initFurnaceMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_trap then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'traps', key = 'CUSTOM_SHIFT_T', | ||||||
|  |             cb = self:callback('initTrapMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_construction then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'constructions', key = 'CUSTOM_SHIFT_C', | ||||||
|  |             cb = self:callback('initConstructionMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_siege then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'siege engine', key = 'CUSTOM_SHIFT_S', | ||||||
|  |             cb = self:callback('initSiegeMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_custom then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'custom workshop', key = 'CUSTOM_SHIFT_U', | ||||||
|  |             cb = self:callback('initCustomMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |      | ||||||
|  |      | ||||||
|  |     | ||||||
|  |     for i=0,df.building_type._last_item do | ||||||
|  |         if (not WORKSHOP_ABSTRACT[i] or self.use_abstract)and not WORKSHOP_SPECIAL[i]  then | ||||||
|  |             self:addBuilding(choices, df.building_type[i], i, -1,-1,nil) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Any building', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:initWorkshopMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i=0,df.workshop_type._last_item do | ||||||
|  |         if i~=df.workshop_type.Custom and (i~=df.workshop_type.Tool or self.use_tool_workshop) then | ||||||
|  |             self:addBuilding(choices, df.workshop_type[i], df.building_type.Workshop, i,-1,nil) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Workshops', choices) | ||||||
|  | end | ||||||
|  | function BuildingDialog:initTrapMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i=0,df.trap_type._last_item do | ||||||
|  |         self:addBuilding(choices, df.trap_type[i], df.building_type.Trap, i,-1,nil) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Traps', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:initConstructionMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i=0,df.construction_type._last_item do | ||||||
|  |         self:addBuilding(choices, df.construction_type[i], df.building_type.Construction, i,-1,nil) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Constructions', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:initFurnaceMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i=0,df.furnace_type._last_item do | ||||||
|  |         self:addBuilding(choices, df.furnace_type[i], df.building_type.Furnace, i,-1,nil) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Furnaces', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:initSiegeMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i=0,df.siegeengine_type._last_item do | ||||||
|  |         self:addBuilding(choices, df.siegeengine_type[i], df.building_type.SiegeEngine, i,-1,nil) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Siege weapons', choices) | ||||||
|  | end | ||||||
|  | function BuildingDialog:initCustomMode() | ||||||
|  |     local choices = {} | ||||||
|  |     local raws=df.global.world.raws.buildings.all | ||||||
|  |     for k,v in pairs(raws) do | ||||||
|  |         self:addBuilding(choices, v.name, df.building_type.Workshop,df.workshop_type.Custom,v.id,v) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Custom workshops', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:addBuilding(choices, name,type_id, subtype_id, custom_id, parent) | ||||||
|  |     -- Check the filter | ||||||
|  |     if self.building_filter and not self.building_filter(name,type_id,subtype_id,custom_id, parent) then | ||||||
|  |         return | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     table.insert(choices, { | ||||||
|  |         text = name:lower(), | ||||||
|  |         customshop = parent, | ||||||
|  |         type_id = type_id, subtype_id = subtype_id, custom_id=custom_id | ||||||
|  |     }) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:pushContext(name, choices) | ||||||
|  |     if not self.back_stack then | ||||||
|  |         self.back_stack = {} | ||||||
|  |         self.subviews.back.visible = false | ||||||
|  |     else | ||||||
|  |         table.insert(self.back_stack, { | ||||||
|  |             context_str = self.context_str, | ||||||
|  |             all_choices = self.subviews.list:getChoices(), | ||||||
|  |             edit_text = self.subviews.list:getFilter(), | ||||||
|  |             selected = self.subviews.list:getSelected(), | ||||||
|  |         }) | ||||||
|  |         self.subviews.back.visible = true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self.context_str = name | ||||||
|  |     self.subviews.list:setChoices(choices, 1) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:onGoBack() | ||||||
|  |     local save = table.remove(self.back_stack) | ||||||
|  |     self.subviews.back.visible = (#self.back_stack > 0) | ||||||
|  | 
 | ||||||
|  |     self.context_str = save.context_str | ||||||
|  |     self.subviews.list:setChoices(save.all_choices) | ||||||
|  |     self.subviews.list:setFilter(save.edit_text, save.selected) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:submitBuilding(type_id,subtype_id,custom_id,choice,index) | ||||||
|  |     self:dismiss() | ||||||
|  | 
 | ||||||
|  |     if self.on_select then | ||||||
|  |         self.on_select(type_id,subtype_id,custom_id,choice,index) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:onSubmitItem(idx, item) | ||||||
|  |     if item.cb then | ||||||
|  |         item:cb(idx) | ||||||
|  |     else | ||||||
|  |         self:submitBuilding(item.type_id, item.subtype_id,item.custom_id,item,idx) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function BuildingDialog:onInput(keys) | ||||||
|  |     if keys.LEAVESCREEN or keys.LEAVESCREEN_ALL then | ||||||
|  |         if self.subviews.back.visible and not keys.LEAVESCREEN_ALL then | ||||||
|  |             self:onGoBack() | ||||||
|  |         else | ||||||
|  |             self:dismiss() | ||||||
|  |             if self.on_cancel then | ||||||
|  |                 self.on_cancel() | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     else | ||||||
|  |         self:inputToSubviews(keys) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function showBuildingPrompt(title, prompt, on_select, on_cancel, build_filter) | ||||||
|  |     BuildingDialog{ | ||||||
|  |         frame_title = title, | ||||||
|  |         prompt = prompt, | ||||||
|  |         building_filter = build_filter, | ||||||
|  |         on_select = on_select, | ||||||
|  |         on_cancel = on_cancel, | ||||||
|  |     }:show() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | return _ENV | ||||||
| @ -0,0 +1,337 @@ | |||||||
|  | -- Stock dialog for selecting materials | ||||||
|  | 
 | ||||||
|  | local _ENV = mkmodule('gui.materials') | ||||||
|  | 
 | ||||||
|  | local gui = require('gui') | ||||||
|  | local widgets = require('gui.widgets') | ||||||
|  | local dlg = require('gui.dialogs') | ||||||
|  | local utils = require('utils') | ||||||
|  | 
 | ||||||
|  | ARROW = string.char(26) | ||||||
|  | 
 | ||||||
|  | CREATURE_BASE = 19 | ||||||
|  | PLANT_BASE = 419 | ||||||
|  | 
 | ||||||
|  | MaterialDialog = defclass(MaterialDialog, gui.FramedScreen) | ||||||
|  | 
 | ||||||
|  | MaterialDialog.focus_path = 'MaterialDialog' | ||||||
|  | 
 | ||||||
|  | MaterialDialog.ATTRS{ | ||||||
|  |     prompt = 'Type or select a material from this list', | ||||||
|  |     frame_style = gui.GREY_LINE_FRAME, | ||||||
|  |     frame_inset = 1, | ||||||
|  |     frame_title = 'Select Material', | ||||||
|  |     -- new attrs | ||||||
|  |     none_caption = 'none', | ||||||
|  |     hide_none = false, | ||||||
|  |     use_inorganic = true, | ||||||
|  |     use_creature = true, | ||||||
|  |     use_plant = true, | ||||||
|  |     mat_filter = DEFAULT_NIL, | ||||||
|  |     on_select = DEFAULT_NIL, | ||||||
|  |     on_cancel = DEFAULT_NIL, | ||||||
|  |     on_close = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:init(info) | ||||||
|  |     self:addviews{ | ||||||
|  |         widgets.Label{ | ||||||
|  |             text = { | ||||||
|  |                 self.prompt, '\n\n', | ||||||
|  |                 'Category: ', { text = self:cb_getfield('context_str'), pen = COLOR_CYAN } | ||||||
|  |             }, | ||||||
|  |             text_pen = COLOR_WHITE, | ||||||
|  |             frame = { l = 0, t = 0 }, | ||||||
|  |         }, | ||||||
|  |         widgets.Label{ | ||||||
|  |             view_id = 'back', | ||||||
|  |             visible = false, | ||||||
|  |             text = { { key = 'LEAVESCREEN', text = ': Back' } }, | ||||||
|  |             frame = { r = 0, b = 0 }, | ||||||
|  |             auto_width = true, | ||||||
|  |         }, | ||||||
|  |         widgets.FilteredList{ | ||||||
|  |             view_id = 'list', | ||||||
|  |             not_found_label = 'No matching materials', | ||||||
|  |             frame = { l = 0, r = 0, t = 4, b = 2 }, | ||||||
|  |             icon_width = 2, | ||||||
|  |             on_submit = self:callback('onSubmitItem'), | ||||||
|  |         }, | ||||||
|  |         widgets.Label{ | ||||||
|  |             text = { { | ||||||
|  |                 key = 'SELECT', text = ': Select', | ||||||
|  |                 disabled = function() return not self.subviews.list:canSubmit() end | ||||||
|  |             } }, | ||||||
|  |             frame = { l = 0, b = 0 }, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     self:initBuiltinMode() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:getWantedFrameSize(rect) | ||||||
|  |     return math.max(self.frame_width or 40, #self.prompt), math.min(28, rect.height-8) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:onDestroy() | ||||||
|  |     if self.on_close then | ||||||
|  |         self.on_close() | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:initBuiltinMode() | ||||||
|  |     local choices = {} | ||||||
|  |     if not self.hide_none then | ||||||
|  |         table.insert(choices, { text = self.none_caption, mat_type = -1, mat_index = -1 }) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     if self.use_inorganic then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'inorganic', key = 'CUSTOM_SHIFT_I', | ||||||
|  |             cb = self:callback('initInorganicMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_creature then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'creature', key = 'CUSTOM_SHIFT_C', | ||||||
|  |             cb = self:callback('initCreatureMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  |     if self.use_plant then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = 'plant', key = 'CUSTOM_SHIFT_P', | ||||||
|  |             cb = self:callback('initPlantMode') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     local table = df.global.world.raws.mat_table.builtin | ||||||
|  |     for i=0,df.builtin_mats._last_item do | ||||||
|  |         self:addMaterial(choices, table[i], i, -1, false, nil) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Any material', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:initInorganicMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i,mat in ipairs(df.global.world.raws.inorganics) do | ||||||
|  |         self:addMaterial(choices, mat.material, 0, i, false, mat) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Inorganic materials', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:initCreatureMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i,v in ipairs(df.global.world.raws.creatures.all) do | ||||||
|  |         self:addObjectChoice(choices, v, v.name[0], CREATURE_BASE, i) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Creature materials', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:initPlantMode() | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     for i,v in ipairs(df.global.world.raws.plants.all) do | ||||||
|  |         self:addObjectChoice(choices, v, v.name, PLANT_BASE, i) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:pushContext('Plant materials', choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:addObjectChoice(choices, obj, name, typ, index) | ||||||
|  |     -- Check if any eligible children | ||||||
|  |     local count = #obj.material | ||||||
|  |     local idx = 0 | ||||||
|  | 
 | ||||||
|  |     if self.mat_filter then | ||||||
|  |         count = 0 | ||||||
|  |         for i,v in ipairs(obj.material) do | ||||||
|  |             if self.mat_filter(v, obj, typ+i, index) then | ||||||
|  |                 count = count + 1 | ||||||
|  |                 if count > 1 then break end | ||||||
|  |                 idx = i | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     -- Create an entry | ||||||
|  |     if count < 1 then | ||||||
|  |         return | ||||||
|  |     elseif count == 1 then | ||||||
|  |         self:addMaterial(choices, obj.material[idx], typ+idx, index, true, obj) | ||||||
|  |     else | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = ARROW, text = name, mat_type = typ, mat_index = index, | ||||||
|  |             obj = obj, cb = self:callback('onSelectObj') | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:onSelectObj(item) | ||||||
|  |     local choices = {} | ||||||
|  |     for i,v in ipairs(item.obj.material) do | ||||||
|  |         self:addMaterial(choices, v, item.mat_type+i, item.mat_index, false, item.obj) | ||||||
|  |     end | ||||||
|  |     self:pushContext(item.text, choices) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:addMaterial(choices, mat, typ, idx, pfix, parent) | ||||||
|  |     -- Check the filter | ||||||
|  |     if self.mat_filter and not self.mat_filter(mat, parent, typ, idx) then | ||||||
|  |         return | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     -- Find the material name | ||||||
|  |     local state = 0 | ||||||
|  |     if mat.heat.melting_point <= 10015 then | ||||||
|  |         state = 1 | ||||||
|  |     end | ||||||
|  |     local name = mat.state_name[state] | ||||||
|  |     name = string.gsub(name, '^frozen ','') | ||||||
|  |     name = string.gsub(name, '^molten ','') | ||||||
|  |     name = string.gsub(name, '^condensed ','') | ||||||
|  | 
 | ||||||
|  |     -- Add prefix if requested | ||||||
|  |     local key | ||||||
|  |     if pfix and mat.prefix ~= '' then | ||||||
|  |         name = mat.prefix .. ' ' .. name | ||||||
|  |         key = mat.prefix | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     table.insert(choices, { | ||||||
|  |         text = name, | ||||||
|  |         search_key = key, | ||||||
|  |         material = mat, | ||||||
|  |         mat_type = typ, mat_index = idx | ||||||
|  |     }) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:pushContext(name, choices) | ||||||
|  |     if not self.back_stack then | ||||||
|  |         self.back_stack = {} | ||||||
|  |         self.subviews.back.visible = false | ||||||
|  |     else | ||||||
|  |         table.insert(self.back_stack, { | ||||||
|  |             context_str = self.context_str, | ||||||
|  |             all_choices = self.subviews.list:getChoices(), | ||||||
|  |             edit_text = self.subviews.list:getFilter(), | ||||||
|  |             selected = self.subviews.list:getSelected(), | ||||||
|  |         }) | ||||||
|  |         self.subviews.back.visible = true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self.context_str = name | ||||||
|  |     self.subviews.list:setChoices(choices, 1) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:onGoBack() | ||||||
|  |     local save = table.remove(self.back_stack) | ||||||
|  |     self.subviews.back.visible = (#self.back_stack > 0) | ||||||
|  | 
 | ||||||
|  |     self.context_str = save.context_str | ||||||
|  |     self.subviews.list:setChoices(save.all_choices) | ||||||
|  |     self.subviews.list:setFilter(save.edit_text, save.selected) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:submitMaterial(typ, index) | ||||||
|  |     self:dismiss() | ||||||
|  | 
 | ||||||
|  |     if self.on_select then | ||||||
|  |         self.on_select(typ, index) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:onSubmitItem(idx, item) | ||||||
|  |     if item.cb then | ||||||
|  |         item:cb(idx) | ||||||
|  |     else | ||||||
|  |         self:submitMaterial(item.mat_type, item.mat_index) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function MaterialDialog:onInput(keys) | ||||||
|  |     if keys.LEAVESCREEN or keys.LEAVESCREEN_ALL then | ||||||
|  |         if self.subviews.back.visible and not keys.LEAVESCREEN_ALL then | ||||||
|  |             self:onGoBack() | ||||||
|  |         else | ||||||
|  |             self:dismiss() | ||||||
|  |             if self.on_cancel then | ||||||
|  |                 self.on_cancel() | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     else | ||||||
|  |         self:inputToSubviews(keys) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function showMaterialPrompt(title, prompt, on_select, on_cancel, mat_filter) | ||||||
|  |     MaterialDialog{ | ||||||
|  |         frame_title = title, | ||||||
|  |         prompt = prompt, | ||||||
|  |         mat_filter = mat_filter, | ||||||
|  |         on_select = on_select, | ||||||
|  |         on_cancel = on_cancel, | ||||||
|  |     }:show() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function ItemTypeDialog(args) | ||||||
|  |     args.text = args.prompt or 'Type or select an item type' | ||||||
|  |     args.text_pen = COLOR_WHITE | ||||||
|  |     args.with_filter = true | ||||||
|  |     args.icon_width = 2 | ||||||
|  | 
 | ||||||
|  |     local choices = {} | ||||||
|  | 
 | ||||||
|  |     if not args.hide_none then | ||||||
|  |         table.insert(choices, { | ||||||
|  |             icon = '?', text = args.none_caption or 'none', | ||||||
|  |             item_type = -1, item_subtype = -1 | ||||||
|  |         }) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     local filter = args.item_filter | ||||||
|  | 
 | ||||||
|  |     for itype = 0,df.item_type._last_item do | ||||||
|  |         local attrs = df.item_type.attrs[itype] | ||||||
|  |         local defcnt = dfhack.items.getSubtypeCount(itype) | ||||||
|  | 
 | ||||||
|  |         if not filter or filter(itype,-1) then | ||||||
|  |             local name = attrs.caption or df.item_type[itype] | ||||||
|  |             local icon | ||||||
|  |             if defcnt >= 0 then | ||||||
|  |                 name = 'any '..name | ||||||
|  |                 icon = '+' | ||||||
|  |             end | ||||||
|  |             table.insert(choices, { | ||||||
|  |                 icon = icon, text = string.lower(name), item_type = itype, item_subtype = -1 | ||||||
|  |             }) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         if defcnt > 0 then | ||||||
|  |             for subtype = 0,defcnt-1 do | ||||||
|  |                 local def = dfhack.items.getSubtypeDef(itype, subtype) | ||||||
|  |                 if not filter or filter(itype,subtype,def) then | ||||||
|  |                     table.insert(choices, { | ||||||
|  |                         icon = '\x1e', text = ' '..def.name, item_type = itype, item_subtype = subtype | ||||||
|  |                     }) | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     args.choices = choices | ||||||
|  | 
 | ||||||
|  |     if args.on_select then | ||||||
|  |         local cb = args.on_select | ||||||
|  |         args.on_select = function(idx, obj) | ||||||
|  |             return cb(obj.item_type, obj.item_subtype) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     return dlg.ListBox(args) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | return _ENV | ||||||
| @ -0,0 +1,164 @@ | |||||||
|  | -- Support for scripted interaction sequences via coroutines. | ||||||
|  | 
 | ||||||
|  | local _ENV = mkmodule('gui.script') | ||||||
|  | 
 | ||||||
|  | local dlg = require('gui.dialogs') | ||||||
|  | 
 | ||||||
|  | --[[ | ||||||
|  |   Example: | ||||||
|  | 
 | ||||||
|  |   start(function() | ||||||
|  |     sleep(100, 'frames') | ||||||
|  |     print(showYesNoPrompt('test', 'print true?')) | ||||||
|  |   end) | ||||||
|  | ]] | ||||||
|  | 
 | ||||||
|  | -- Table of running background scripts. | ||||||
|  | if not scripts then | ||||||
|  |     scripts = {} | ||||||
|  |     setmetatable(scripts, { __mode = 'k' }) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function do_resume(inst, ...) | ||||||
|  |     inst.gen = inst.gen + 1 | ||||||
|  |     return (dfhack.saferesume(inst.coro, ...)) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Starts a new background script by calling the function. | ||||||
|  | function start(fn,...) | ||||||
|  |     local coro = coroutine.create(fn) | ||||||
|  |     local inst = { | ||||||
|  |         coro = coro, | ||||||
|  |         gen = 0, | ||||||
|  |     } | ||||||
|  |     scripts[coro] = inst | ||||||
|  |     return do_resume(inst, ...) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Checks if called from a background script | ||||||
|  | function in_script() | ||||||
|  |     return scripts[coroutine.running()] ~= nil | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function getinst() | ||||||
|  |     local inst = scripts[coroutine.running()] | ||||||
|  |     if not inst then | ||||||
|  |         error('Not in a gui script coroutine.') | ||||||
|  |     end | ||||||
|  |     return inst | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function invoke_resume(inst,gen,quiet,...) | ||||||
|  |     local state = coroutine.status(inst.coro) | ||||||
|  |     if state ~= 'suspended' then | ||||||
|  |         if state ~= 'dead' then | ||||||
|  |             dfhack.printerr(debug.traceback('resuming a non-waiting continuation')) | ||||||
|  |         end | ||||||
|  |     elseif inst.gen > gen then | ||||||
|  |         if not quiet then | ||||||
|  |             dfhack.printerr(debug.traceback('resuming an expired continuation')) | ||||||
|  |         end | ||||||
|  |     else | ||||||
|  |         do_resume(inst, ...) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Returns a callback that resumes the script from wait with given return values | ||||||
|  | function mkresume(...) | ||||||
|  |     local inst = getinst() | ||||||
|  |     return curry(invoke_resume, inst, inst.gen, false, ...) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Like mkresume, but does not complain if already resumed from this wait | ||||||
|  | function qresume(...) | ||||||
|  |     local inst = getinst() | ||||||
|  |     return curry(invoke_resume, inst, inst.gen, true, ...) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Wait until a mkresume callback is called, then return its arguments. | ||||||
|  | -- Once it returns, all mkresume callbacks created before are invalidated. | ||||||
|  | function wait() | ||||||
|  |     getinst() -- check that the context is right | ||||||
|  |     return coroutine.yield() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Wraps dfhack.timeout for coroutines. | ||||||
|  | function sleep(time, quantity) | ||||||
|  |     if dfhack.timeout(time, quantity, mkresume()) then | ||||||
|  |         wait() | ||||||
|  |         return true | ||||||
|  |     else | ||||||
|  |         return false | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | -- Some dialog wrappers: | ||||||
|  | 
 | ||||||
|  | function showMessage(title, text, tcolor) | ||||||
|  |     dlg.MessageBox{ | ||||||
|  |         frame_title = title, | ||||||
|  |         text = text, | ||||||
|  |         text_pen = tcolor, | ||||||
|  |         on_close = qresume(nil) | ||||||
|  |     }:show() | ||||||
|  | 
 | ||||||
|  |     return wait() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function showYesNoPrompt(title, text, tcolor) | ||||||
|  |     dlg.MessageBox{ | ||||||
|  |         frame_title = title, | ||||||
|  |         text = text, | ||||||
|  |         text_pen = tcolor, | ||||||
|  |         on_accept = mkresume(true), | ||||||
|  |         on_cancel = mkresume(false), | ||||||
|  |         on_close = qresume(nil) | ||||||
|  |     }:show() | ||||||
|  | 
 | ||||||
|  |     return wait() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function showInputPrompt(title, text, tcolor, input, min_width) | ||||||
|  |     dlg.InputBox{ | ||||||
|  |         frame_title = title, | ||||||
|  |         text = text, | ||||||
|  |         text_pen = tcolor, | ||||||
|  |         input = input, | ||||||
|  |         frame_width = min_width, | ||||||
|  |         on_input = mkresume(true), | ||||||
|  |         on_cancel = mkresume(false), | ||||||
|  |         on_close = qresume(nil) | ||||||
|  |     }:show() | ||||||
|  | 
 | ||||||
|  |     return wait() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function showListPrompt(title, text, tcolor, choices, min_width, filter) | ||||||
|  |     dlg.ListBox{ | ||||||
|  |         frame_title = title, | ||||||
|  |         text = text, | ||||||
|  |         text_pen = tcolor, | ||||||
|  |         choices = choices, | ||||||
|  |         frame_width = min_width, | ||||||
|  |         with_filter = filter, | ||||||
|  |         on_select = mkresume(true), | ||||||
|  |         on_cancel = mkresume(false), | ||||||
|  |         on_close = qresume(nil) | ||||||
|  |     }:show() | ||||||
|  | 
 | ||||||
|  |     return wait() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function showMaterialPrompt(title, prompt) | ||||||
|  |     require('gui.materials').MaterialDialog{ | ||||||
|  |         frame_title = title, | ||||||
|  |         prompt = prompt, | ||||||
|  |         on_select = mkresume(true, | ||||||
|  |         on_cancel = mkresume(false), | ||||||
|  |         on_close = qresume(nil) | ||||||
|  |     }:show() | ||||||
|  | 
 | ||||||
|  |     return wait() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | return _ENV | ||||||
| @ -0,0 +1,783 @@ | |||||||
|  | -- Simple widgets for screens | ||||||
|  | 
 | ||||||
|  | local _ENV = mkmodule('gui.widgets') | ||||||
|  | 
 | ||||||
|  | local gui = require('gui') | ||||||
|  | local utils = require('utils') | ||||||
|  | 
 | ||||||
|  | local dscreen = dfhack.screen | ||||||
|  | 
 | ||||||
|  | local function show_view(view,vis) | ||||||
|  |     if view then | ||||||
|  |         view.visible = vis | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function getval(obj) | ||||||
|  |     if type(obj) == 'function' then | ||||||
|  |         return obj() | ||||||
|  |     else | ||||||
|  |         return obj | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function map_opttab(tab,idx) | ||||||
|  |     if tab then | ||||||
|  |         return tab[idx] | ||||||
|  |     else | ||||||
|  |         return idx | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ------------ | ||||||
|  | -- Widget -- | ||||||
|  | ------------ | ||||||
|  | 
 | ||||||
|  | Widget = defclass(Widget, gui.View) | ||||||
|  | 
 | ||||||
|  | Widget.ATTRS { | ||||||
|  |     frame = DEFAULT_NIL, | ||||||
|  |     frame_inset = DEFAULT_NIL, | ||||||
|  |     frame_background = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function Widget:computeFrame(parent_rect) | ||||||
|  |     local sw, sh = parent_rect.width, parent_rect.height | ||||||
|  |     return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Widget:onRenderFrame(dc, rect) | ||||||
|  |     if self.frame_background then | ||||||
|  |         dc:fill(rect, self.frame_background) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ----------- | ||||||
|  | -- Panel -- | ||||||
|  | ----------- | ||||||
|  | 
 | ||||||
|  | Panel = defclass(Panel, Widget) | ||||||
|  | 
 | ||||||
|  | Panel.ATTRS { | ||||||
|  |     on_render = DEFAULT_NIL, | ||||||
|  |     on_layout = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function Panel:init(args) | ||||||
|  |     self:addviews(args.subviews) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Panel:onRenderBody(dc) | ||||||
|  |     if self.on_render then self.on_render(dc) end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Panel:postComputeFrame(body) | ||||||
|  |     if self.on_layout then self.on_layout(body) end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ----------- | ||||||
|  | -- Pages -- | ||||||
|  | ----------- | ||||||
|  | 
 | ||||||
|  | Pages = defclass(Pages, Panel) | ||||||
|  | 
 | ||||||
|  | function Pages:init(args) | ||||||
|  |     for _,v in ipairs(self.subviews) do | ||||||
|  |         v.visible = false | ||||||
|  |     end | ||||||
|  |     self:setSelected(args.selected or 1) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Pages:setSelected(idx) | ||||||
|  |     if type(idx) ~= 'number' then | ||||||
|  |         local key = idx | ||||||
|  |         if type(idx) == 'string' then | ||||||
|  |             key = self.subviews[key] | ||||||
|  |         end | ||||||
|  |         idx = utils.linear_index(self.subviews, key) | ||||||
|  |         if not idx then | ||||||
|  |             error('Unknown page: '..key) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     show_view(self.subviews[self.selected], false) | ||||||
|  |     self.selected = math.min(math.max(1, idx), #self.subviews) | ||||||
|  |     show_view(self.subviews[self.selected], true) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Pages:getSelected() | ||||||
|  |     return self.selected, self.subviews[self.selected] | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ---------------- | ||||||
|  | -- Edit field -- | ||||||
|  | ---------------- | ||||||
|  | 
 | ||||||
|  | EditField = defclass(EditField, Widget) | ||||||
|  | 
 | ||||||
|  | EditField.ATTRS{ | ||||||
|  |     text = '', | ||||||
|  |     text_pen = DEFAULT_NIL, | ||||||
|  |     on_char = DEFAULT_NIL, | ||||||
|  |     on_change = DEFAULT_NIL, | ||||||
|  |     on_submit = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function EditField:onRenderBody(dc) | ||||||
|  |     dc:pen(self.text_pen or COLOR_LIGHTCYAN):fill(0,0,dc.width-1,0) | ||||||
|  | 
 | ||||||
|  |     local cursor = '_' | ||||||
|  |     if not self.active or gui.blink_visible(300) then | ||||||
|  |         cursor = ' ' | ||||||
|  |     end | ||||||
|  |     local txt = self.text .. cursor | ||||||
|  |     if #txt > dc.width then | ||||||
|  |         txt = string.char(27)..string.sub(txt, #txt-dc.width+2) | ||||||
|  |     end | ||||||
|  |     dc:string(txt) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function EditField:onInput(keys) | ||||||
|  |     if self.on_submit and keys.SELECT then | ||||||
|  |         self.on_submit(self.text) | ||||||
|  |         return true | ||||||
|  |     elseif keys._STRING then | ||||||
|  |         local old = self.text | ||||||
|  |         if keys._STRING == 0 then | ||||||
|  |             self.text = string.sub(old, 1, #old-1) | ||||||
|  |         else | ||||||
|  |             local cv = string.char(keys._STRING) | ||||||
|  |             if not self.on_char or self.on_char(cv, old) then | ||||||
|  |                 self.text = old .. cv | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |         if self.on_change and self.text ~= old then | ||||||
|  |             self.on_change(self.text, old) | ||||||
|  |         end | ||||||
|  |         return true | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ----------- | ||||||
|  | -- Label -- | ||||||
|  | ----------- | ||||||
|  | 
 | ||||||
|  | function parse_label_text(obj) | ||||||
|  |     local text = obj.text or {} | ||||||
|  |     if type(text) ~= 'table' then | ||||||
|  |         text = { text } | ||||||
|  |     end | ||||||
|  |     local curline = nil | ||||||
|  |     local out = { } | ||||||
|  |     local active = nil | ||||||
|  |     local idtab = nil | ||||||
|  |     for _,v in ipairs(text) do | ||||||
|  |         local vv | ||||||
|  |         if type(v) == 'string' then | ||||||
|  |             vv = utils.split_string(v, NEWLINE) | ||||||
|  |         else | ||||||
|  |             vv = { v } | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         for i = 1,#vv do | ||||||
|  |             local cv = vv[i] | ||||||
|  |             if i > 1 then | ||||||
|  |                 if not curline then | ||||||
|  |                     table.insert(out, {}) | ||||||
|  |                 end | ||||||
|  |                 curline = nil | ||||||
|  |             end | ||||||
|  |             if cv ~= '' then | ||||||
|  |                 if not curline then | ||||||
|  |                     curline = {} | ||||||
|  |                     table.insert(out, curline) | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 if type(cv) == 'string' then | ||||||
|  |                     table.insert(curline, { text = cv }) | ||||||
|  |                 else | ||||||
|  |                     table.insert(curline, cv) | ||||||
|  | 
 | ||||||
|  |                     if cv.on_activate then | ||||||
|  |                         active = active or {} | ||||||
|  |                         table.insert(active, cv) | ||||||
|  |                     end | ||||||
|  | 
 | ||||||
|  |                     if cv.id then | ||||||
|  |                         idtab = idtab or {} | ||||||
|  |                         idtab[cv.id] = cv | ||||||
|  |                     end | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     obj.text_lines = out | ||||||
|  |     obj.text_active = active | ||||||
|  |     obj.text_ids = idtab | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local function is_disabled(token) | ||||||
|  |     return (token.disabled ~= nil and getval(token.disabled)) or | ||||||
|  |            (token.enabled ~= nil and not getval(token.enabled)) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function render_text(obj,dc,x0,y0,pen,dpen,disabled) | ||||||
|  |     local width = 0 | ||||||
|  |     for iline,line in ipairs(obj.text_lines) do | ||||||
|  |         local x = 0 | ||||||
|  |         if dc then | ||||||
|  |             dc:seek(x+x0,y0+iline-1) | ||||||
|  |         end | ||||||
|  |         for _,token in ipairs(line) do | ||||||
|  |             token.line = iline | ||||||
|  |             token.x1 = x | ||||||
|  | 
 | ||||||
|  |             if token.gap then | ||||||
|  |                 x = x + token.gap | ||||||
|  |                 if dc then | ||||||
|  |                     dc:advance(token.gap) | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             if token.tile then | ||||||
|  |                 x = x + 1 | ||||||
|  |                 if dc then | ||||||
|  |                     dc:char(nil, token.tile) | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             if token.text or token.key then | ||||||
|  |                 local text = ''..(getval(token.text) or '') | ||||||
|  |                 local keypen | ||||||
|  | 
 | ||||||
|  |                 if dc then | ||||||
|  |                     local tpen = getval(token.pen) | ||||||
|  |                     if disabled or is_disabled(token) then | ||||||
|  |                         dc:pen(getval(token.dpen) or tpen or dpen) | ||||||
|  |                         keypen = COLOR_GREEN | ||||||
|  |                     else | ||||||
|  |                         dc:pen(tpen or pen) | ||||||
|  |                         keypen = COLOR_LIGHTGREEN | ||||||
|  |                     end | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 local width = getval(token.width) | ||||||
|  |                 local padstr | ||||||
|  |                 if width then | ||||||
|  |                     x = x + width | ||||||
|  |                     if #text > width then | ||||||
|  |                         text = string.sub(text,1,width) | ||||||
|  |                     else | ||||||
|  |                         if token.pad_char then | ||||||
|  |                             padstr = string.rep(token.pad_char,width-#text) | ||||||
|  |                         end | ||||||
|  |                         if dc and token.rjustify then | ||||||
|  |                             if padstr then dc:string(padstr) else dc:advance(width-#text) end | ||||||
|  |                         end | ||||||
|  |                     end | ||||||
|  |                 else | ||||||
|  |                     x = x + #text | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 if token.key then | ||||||
|  |                     local keystr = gui.getKeyDisplay(token.key) | ||||||
|  |                     local sep = token.key_sep or '' | ||||||
|  | 
 | ||||||
|  |                     x = x + #keystr | ||||||
|  | 
 | ||||||
|  |                     if sep == '()' then | ||||||
|  |                         if dc then | ||||||
|  |                             dc:string(text) | ||||||
|  |                             dc:string(' ('):string(keystr,keypen):string(')') | ||||||
|  |                         end | ||||||
|  |                         x = x + 3 | ||||||
|  |                     else | ||||||
|  |                         if dc then | ||||||
|  |                             dc:string(keystr,keypen):string(sep):string(text) | ||||||
|  |                         end | ||||||
|  |                         x = x + #sep | ||||||
|  |                     end | ||||||
|  |                 else | ||||||
|  |                     if dc then | ||||||
|  |                         dc:string(text) | ||||||
|  |                     end | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 if width and dc and not token.rjustify then | ||||||
|  |                     if padstr then dc:string(padstr) else dc:advance(width-#text) end | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  | 
 | ||||||
|  |             token.x2 = x | ||||||
|  |         end | ||||||
|  |         width = math.max(width, x) | ||||||
|  |     end | ||||||
|  |     obj.text_width = width | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function check_text_keys(self, keys) | ||||||
|  |     if self.text_active then | ||||||
|  |         for _,item in ipairs(self.text_active) do | ||||||
|  |             if item.key and keys[item.key] and not is_disabled(item) then | ||||||
|  |                 item.on_activate() | ||||||
|  |                 return true | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | Label = defclass(Label, Widget) | ||||||
|  | 
 | ||||||
|  | Label.ATTRS{ | ||||||
|  |     text_pen = COLOR_WHITE, | ||||||
|  |     text_dpen = COLOR_DARKGREY, | ||||||
|  |     disabled = DEFAULT_NIL, | ||||||
|  |     enabled = DEFAULT_NIL, | ||||||
|  |     auto_height = true, | ||||||
|  |     auto_width = false, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function Label:init(args) | ||||||
|  |     self:setText(args.text) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:setText(text) | ||||||
|  |     self.text = text | ||||||
|  |     parse_label_text(self) | ||||||
|  | 
 | ||||||
|  |     if self.auto_height then | ||||||
|  |         self.frame = self.frame or {} | ||||||
|  |         self.frame.h = self:getTextHeight() | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:preUpdateLayout() | ||||||
|  |     if self.auto_width then | ||||||
|  |         self.frame = self.frame or {} | ||||||
|  |         self.frame.w = self:getTextWidth() | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:itemById(id) | ||||||
|  |     if self.text_ids then | ||||||
|  |         return self.text_ids[id] | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:getTextHeight() | ||||||
|  |     return #self.text_lines | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:getTextWidth() | ||||||
|  |     render_text(self) | ||||||
|  |     return self.text_width | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:onRenderBody(dc) | ||||||
|  |     render_text(self,dc,0,0,self.text_pen,self.text_dpen,is_disabled(self)) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function Label:onInput(keys) | ||||||
|  |     if not is_disabled(self) then | ||||||
|  |         return check_text_keys(self, keys) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ---------- | ||||||
|  | -- List -- | ||||||
|  | ---------- | ||||||
|  | 
 | ||||||
|  | List = defclass(List, Widget) | ||||||
|  | 
 | ||||||
|  | STANDARDSCROLL = { | ||||||
|  |     STANDARDSCROLL_UP = -1, | ||||||
|  |     STANDARDSCROLL_DOWN = 1, | ||||||
|  |     STANDARDSCROLL_PAGEUP = '-page', | ||||||
|  |     STANDARDSCROLL_PAGEDOWN = '+page', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | SECONDSCROLL = { | ||||||
|  |     SECONDSCROLL_UP = -1, | ||||||
|  |     SECONDSCROLL_DOWN = 1, | ||||||
|  |     SECONDSCROLL_PAGEUP = '-page', | ||||||
|  |     SECONDSCROLL_PAGEDOWN = '+page', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | List.ATTRS{ | ||||||
|  |     text_pen = COLOR_CYAN, | ||||||
|  |     cursor_pen = COLOR_LIGHTCYAN, | ||||||
|  |     inactive_pen = DEFAULT_NIL, | ||||||
|  |     on_select = DEFAULT_NIL, | ||||||
|  |     on_submit = DEFAULT_NIL, | ||||||
|  |     on_submit2 = DEFAULT_NIL, | ||||||
|  |     row_height = 1, | ||||||
|  |     scroll_keys = STANDARDSCROLL, | ||||||
|  |     icon_width = DEFAULT_NIL, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function List:init(info) | ||||||
|  |     self.page_top = 1 | ||||||
|  |     self.page_size = 1 | ||||||
|  | 
 | ||||||
|  |     if info.choices then | ||||||
|  |         self:setChoices(info.choices, info.selected) | ||||||
|  |     else | ||||||
|  |         self.choices = {} | ||||||
|  |         self.selected = 1 | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:setChoices(choices, selected) | ||||||
|  |     self.choices = choices or {} | ||||||
|  | 
 | ||||||
|  |     for i,v in ipairs(self.choices) do | ||||||
|  |         if type(v) ~= 'table' then | ||||||
|  |             v = { text = v } | ||||||
|  |             self.choices[i] = v | ||||||
|  |         end | ||||||
|  |         v.text = v.text or v.caption or v[1] | ||||||
|  |         parse_label_text(v) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self:setSelected(selected) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:setSelected(selected) | ||||||
|  |     self.selected = selected or self.selected or 1 | ||||||
|  |     self:moveCursor(0, true) | ||||||
|  |     return self.selected | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:getChoices() | ||||||
|  |     return self.choices | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:getSelected() | ||||||
|  |     if #self.choices > 0 then | ||||||
|  |         return self.selected, self.choices[self.selected] | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:getContentWidth() | ||||||
|  |     local width = 0 | ||||||
|  |     for i,v in ipairs(self.choices) do | ||||||
|  |         render_text(v) | ||||||
|  |         local roww = v.text_width | ||||||
|  |         if v.key then | ||||||
|  |             roww = roww + 3 + #gui.getKeyDisplay(v.key) | ||||||
|  |         end | ||||||
|  |         width = math.max(width, roww) | ||||||
|  |     end | ||||||
|  |     return width + (self.icon_width or 0) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:getContentHeight() | ||||||
|  |     return #self.choices * self.row_height | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:postComputeFrame(body) | ||||||
|  |     self.page_size = math.max(1, math.floor(body.height / self.row_height)) | ||||||
|  |     self:moveCursor(0) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:moveCursor(delta, force_cb) | ||||||
|  |     local page = math.max(1, self.page_size) | ||||||
|  |     local cnt = #self.choices | ||||||
|  | 
 | ||||||
|  |     if cnt < 1 then | ||||||
|  |         self.page_top = 1 | ||||||
|  |         self.selected = 1 | ||||||
|  |         if force_cb and self.on_select then | ||||||
|  |             self.on_select(nil,nil) | ||||||
|  |         end | ||||||
|  |         return | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     local off = self.selected+delta-1 | ||||||
|  |     local ds = math.abs(delta) | ||||||
|  | 
 | ||||||
|  |     if ds > 1 then | ||||||
|  |         if off >= cnt+ds-1 then | ||||||
|  |             off = 0 | ||||||
|  |         else | ||||||
|  |             off = math.min(cnt-1, off) | ||||||
|  |         end | ||||||
|  |         if off <= -ds then | ||||||
|  |             off = cnt-1 | ||||||
|  |         else | ||||||
|  |             off = math.max(0, off) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self.selected = 1 + off % cnt | ||||||
|  |     self.page_top = 1 + page * math.floor((self.selected-1) / page) | ||||||
|  | 
 | ||||||
|  |     if (force_cb or delta ~= 0) and self.on_select then | ||||||
|  |         self.on_select(self:getSelected()) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:onRenderBody(dc) | ||||||
|  |     local choices = self.choices | ||||||
|  |     local top = self.page_top | ||||||
|  |     local iend = math.min(#choices, top+self.page_size-1) | ||||||
|  |     local iw = self.icon_width | ||||||
|  | 
 | ||||||
|  |     local function paint_icon(icon, obj) | ||||||
|  |         if type(icon) ~= 'string' then | ||||||
|  |             dc:char(nil,icon) | ||||||
|  |         else | ||||||
|  |             if current then | ||||||
|  |                 dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen) | ||||||
|  |             else | ||||||
|  |                 dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen) | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     for i = top,iend do | ||||||
|  |         local obj = choices[i] | ||||||
|  |         local current = (i == self.selected) | ||||||
|  |         local cur_pen = self.cursor_pen | ||||||
|  |         local cur_dpen = self.text_pen | ||||||
|  | 
 | ||||||
|  |         if not self.active then | ||||||
|  |             cur_pen = self.inactive_pen or self.cursor_pen | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         local y = (i - top)*self.row_height | ||||||
|  |         local icon = getval(obj.icon) | ||||||
|  | 
 | ||||||
|  |         if iw and icon then | ||||||
|  |             dc:seek(0, y) | ||||||
|  |             paint_icon(icon, obj) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current) | ||||||
|  | 
 | ||||||
|  |         local ip = dc.width | ||||||
|  | 
 | ||||||
|  |         if obj.key then | ||||||
|  |             local keystr = gui.getKeyDisplay(obj.key) | ||||||
|  |             ip = ip-2-#keystr | ||||||
|  |             dc:seek(ip,y):pen(self.text_pen) | ||||||
|  |             dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')') | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         if icon and not iw then | ||||||
|  |             dc:seek(ip-1,y) | ||||||
|  |             paint_icon(icon, obj) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:submit() | ||||||
|  |     if self.on_submit and #self.choices > 0 then | ||||||
|  |         self.on_submit(self:getSelected()) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:submit2() | ||||||
|  |     if self.on_submit2 and #self.choices > 0 then | ||||||
|  |         self.on_submit2(self:getSelected()) | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function List:onInput(keys) | ||||||
|  |     if self.on_submit and keys.SELECT then | ||||||
|  |         self:submit() | ||||||
|  |         return true | ||||||
|  |     elseif self.on_submit2 and keys.SEC_SELECT then | ||||||
|  |         self:submit2() | ||||||
|  |         return true | ||||||
|  |     else | ||||||
|  |         for k,v in pairs(self.scroll_keys) do | ||||||
|  |             if keys[k] then | ||||||
|  |                 if v == '+page' then | ||||||
|  |                     v = self.page_size | ||||||
|  |                 elseif v == '-page' then | ||||||
|  |                     v = -self.page_size | ||||||
|  |                 end | ||||||
|  | 
 | ||||||
|  |                 self:moveCursor(v) | ||||||
|  |                 return true | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         for i,v in ipairs(self.choices) do | ||||||
|  |             if v.key and keys[v.key] then | ||||||
|  |                 self:setSelected(i) | ||||||
|  |                 self:submit() | ||||||
|  |                 return true | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         local current = self.choices[self.selected] | ||||||
|  |         if current then | ||||||
|  |             return check_text_keys(current, keys) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | ------------------- | ||||||
|  | -- Filtered List -- | ||||||
|  | ------------------- | ||||||
|  | 
 | ||||||
|  | FilteredList = defclass(FilteredList, Widget) | ||||||
|  | 
 | ||||||
|  | FilteredList.ATTRS { | ||||||
|  |     edit_below = false, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function FilteredList:init(info) | ||||||
|  |     self.edit = EditField{ | ||||||
|  |         text_pen = info.edit_pen or info.cursor_pen, | ||||||
|  |         frame = { l = info.icon_width, t = 0, h = 1 }, | ||||||
|  |         on_change = self:callback('onFilterChange'), | ||||||
|  |         on_char = self:callback('onFilterChar'), | ||||||
|  |     } | ||||||
|  |     self.list = List{ | ||||||
|  |         frame = { t = 2 }, | ||||||
|  |         text_pen = info.text_pen, | ||||||
|  |         cursor_pen = info.cursor_pen, | ||||||
|  |         inactive_pen = info.inactive_pen, | ||||||
|  |         icon_pen = info.icon_pen, | ||||||
|  |         row_height = info.row_height, | ||||||
|  |         scroll_keys = info.scroll_keys, | ||||||
|  |         icon_width = info.icon_width, | ||||||
|  |     } | ||||||
|  |     if self.edit_below then | ||||||
|  |         self.edit.frame = { l = info.icon_width, b = 0, h = 1 } | ||||||
|  |         self.list.frame = { t = 0, b = 2 } | ||||||
|  |     end | ||||||
|  |     if info.on_select then | ||||||
|  |         self.list.on_select = function() | ||||||
|  |             return info.on_select(self:getSelected()) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     if info.on_submit then | ||||||
|  |         self.list.on_submit = function() | ||||||
|  |             return info.on_submit(self:getSelected()) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     if info.on_submit2 then | ||||||
|  |         self.list.on_submit2 = function() | ||||||
|  |             return info.on_submit2(self:getSelected()) | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  |     self.not_found = Label{ | ||||||
|  |         visible = true, | ||||||
|  |         text = info.not_found_label or 'No matches', | ||||||
|  |         text_pen = COLOR_LIGHTRED, | ||||||
|  |         frame = { l = info.icon_width, t = self.list.frame.t }, | ||||||
|  |     } | ||||||
|  |     self:addviews{ self.edit, self.list, self.not_found } | ||||||
|  |     if info.choices then | ||||||
|  |         self:setChoices(info.choices, info.selected) | ||||||
|  |     else | ||||||
|  |         self.choices = {} | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:getChoices() | ||||||
|  |     return self.choices | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:setChoices(choices, pos) | ||||||
|  |     choices = choices or {} | ||||||
|  |     self.choices = choices | ||||||
|  |     self.edit.text = '' | ||||||
|  |     self.list:setChoices(choices, pos) | ||||||
|  |     self.not_found.visible = (#choices == 0) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:submit() | ||||||
|  |     return self.list:submit() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:submit2() | ||||||
|  |     return self.list:submit2() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:canSubmit() | ||||||
|  |     return not self.not_found.visible | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:getSelected() | ||||||
|  |     local i,v = self.list:getSelected() | ||||||
|  |     if i then | ||||||
|  |         return map_opttab(self.choice_index, i), v | ||||||
|  |     end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:getContentWidth() | ||||||
|  |     return self.list:getContentWidth() | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:getContentHeight() | ||||||
|  |     return self.list:getContentHeight() + 2 | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:getFilter() | ||||||
|  |     return self.edit.text, self.list.choices | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:setFilter(filter, pos) | ||||||
|  |     local choices = self.choices | ||||||
|  |     local cidx = nil | ||||||
|  | 
 | ||||||
|  |     filter = filter or '' | ||||||
|  |     self.edit.text = filter | ||||||
|  | 
 | ||||||
|  |     if filter ~= '' then | ||||||
|  |         local tokens = utils.split_string(filter, ' ') | ||||||
|  |         local ipos = pos | ||||||
|  | 
 | ||||||
|  |         choices = {} | ||||||
|  |         cidx = {} | ||||||
|  |         pos = nil | ||||||
|  | 
 | ||||||
|  |         for i,v in ipairs(self.choices) do | ||||||
|  |             local ok = true | ||||||
|  |             local search_key = v.search_key or v.text | ||||||
|  |             for _,key in ipairs(tokens) do | ||||||
|  |                 if key ~= '' and not string.match(search_key, '%f[^%s\x00]'..key) then | ||||||
|  |                     ok = false | ||||||
|  |                     break | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  |             if ok then | ||||||
|  |                 table.insert(choices, v) | ||||||
|  |                 cidx[#choices] = i | ||||||
|  |                 if ipos == i then | ||||||
|  |                     pos = #choices | ||||||
|  |                 end | ||||||
|  |             end | ||||||
|  |         end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     self.choice_index = cidx | ||||||
|  |     self.list:setChoices(choices, pos) | ||||||
|  |     self.not_found.visible = (#choices == 0) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | function FilteredList:onFilterChange(text) | ||||||
|  |     self:setFilter(text) | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | local bad_chars = { | ||||||
|  |     ['%'] = true, ['.'] = true, ['+'] = true, ['*'] = true, | ||||||
|  |     ['['] = true, [']'] = true, ['('] = true, [')'] = true, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function FilteredList:onFilterChar(char, text) | ||||||
|  |     if bad_chars[char] then | ||||||
|  |         return false | ||||||
|  |     end | ||||||
|  |     if char == ' ' then | ||||||
|  |         return string.match(text, '%S$') | ||||||
|  |     end | ||||||
|  |     return true | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | return _ENV | ||||||
| @ -0,0 +1,503 @@ | |||||||
|  | #include "Core.h" | ||||||
|  | #include "Console.h" | ||||||
|  | #include "modules/Buildings.h" | ||||||
|  | #include "modules/Constructions.h" | ||||||
|  | #include "modules/EventManager.h" | ||||||
|  | #include "modules/Job.h" | ||||||
|  | #include "modules/World.h" | ||||||
|  | 
 | ||||||
|  | #include "df/building.h" | ||||||
|  | #include "df/construction.h" | ||||||
|  | #include "df/global_objects.h" | ||||||
|  | #include "df/item.h" | ||||||
|  | #include "df/job.h" | ||||||
|  | #include "df/job_list_link.h" | ||||||
|  | #include "df/ui.h" | ||||||
|  | #include "df/unit.h" | ||||||
|  | #include "df/unit_syndrome.h" | ||||||
|  | #include "df/world.h" | ||||||
|  | 
 | ||||||
|  | #include <map> | ||||||
|  | #include <unordered_map> | ||||||
|  | #include <unordered_set> | ||||||
|  | 
 | ||||||
|  | using namespace std; | ||||||
|  | using namespace DFHack; | ||||||
|  | using namespace EventManager; | ||||||
|  | 
 | ||||||
|  | /*
 | ||||||
|  |  * TODO: | ||||||
|  |  *  error checking | ||||||
|  |  *  consider a typedef instead of a struct for EventHandler | ||||||
|  |  **/ | ||||||
|  | 
 | ||||||
|  | //map<uint32_t, vector<DFHack::EventManager::EventHandler> > tickQueue;
 | ||||||
|  | multimap<uint32_t, EventHandler> tickQueue; | ||||||
|  | 
 | ||||||
|  | //TODO: consider unordered_map of pairs, or unordered_map of unordered_set, or whatever
 | ||||||
|  | multimap<Plugin*, EventHandler> handlers[EventType::EVENT_MAX]; | ||||||
|  | uint32_t eventLastTick[EventType::EVENT_MAX]; | ||||||
|  | 
 | ||||||
|  | const uint32_t ticksPerYear = 403200; | ||||||
|  | 
 | ||||||
|  | void DFHack::EventManager::registerListener(EventType::EventType e, EventHandler handler, Plugin* plugin) { | ||||||
|  |     handlers[e].insert(pair<Plugin*, EventHandler>(plugin, handler)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void DFHack::EventManager::registerTick(EventHandler handler, int32_t when, Plugin* plugin, bool absolute) { | ||||||
|  |     uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear | ||||||
|  |         + DFHack::World::ReadCurrentTick(); | ||||||
|  |     if ( !Core::getInstance().isWorldLoaded() ) { | ||||||
|  |         tick = 0; | ||||||
|  |         if ( absolute ) { | ||||||
|  |             Core::getInstance().getConsole().print("Warning: absolute flag will not be honored.\n"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if ( absolute ) { | ||||||
|  |         tick = 0; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     tickQueue.insert(pair<uint32_t, EventHandler>(tick+(uint32_t)when, handler)); | ||||||
|  |     handlers[EventType::TICK].insert(pair<Plugin*,EventHandler>(plugin,handler)); | ||||||
|  |     return; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void DFHack::EventManager::unregister(EventType::EventType e, EventHandler handler, Plugin* plugin) { | ||||||
|  |     for ( multimap<Plugin*, EventHandler>::iterator i = handlers[e].find(plugin); i != handlers[e].end(); i++ ) { | ||||||
|  |         if ( (*i).first != plugin ) | ||||||
|  |             break; | ||||||
|  |         EventHandler handle = (*i).second; | ||||||
|  |         if ( handle == handler ) { | ||||||
|  |             handlers[e].erase(i); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void DFHack::EventManager::unregisterAll(Plugin* plugin) { | ||||||
|  |     for ( auto i = handlers[EventType::TICK].find(plugin); i != handlers[EventType::TICK].end(); i++ ) { | ||||||
|  |         if ( (*i).first != plugin ) | ||||||
|  |             break; | ||||||
|  |          | ||||||
|  |         //shenanigans to avoid concurrent modification
 | ||||||
|  |         EventHandler getRidOf = (*i).second; | ||||||
|  |         bool didSomething; | ||||||
|  |         do { | ||||||
|  |             didSomething = false; | ||||||
|  |             for ( auto j = tickQueue.begin(); j != tickQueue.end(); j++ ) { | ||||||
|  |                 EventHandler candidate = (*j).second; | ||||||
|  |                 if ( getRidOf != candidate ) | ||||||
|  |                     continue; | ||||||
|  |                 tickQueue.erase(j); | ||||||
|  |                 didSomething = true; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } while(didSomething); | ||||||
|  |     } | ||||||
|  |     for ( size_t a = 0; a < (size_t)EventType::EVENT_MAX; a++ ) { | ||||||
|  |         handlers[a].erase(plugin); | ||||||
|  |     } | ||||||
|  |     return; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageTickEvent(color_ostream& out); | ||||||
|  | static void manageJobInitiatedEvent(color_ostream& out); | ||||||
|  | static void manageJobCompletedEvent(color_ostream& out); | ||||||
|  | static void manageUnitDeathEvent(color_ostream& out); | ||||||
|  | static void manageItemCreationEvent(color_ostream& out); | ||||||
|  | static void manageBuildingEvent(color_ostream& out); | ||||||
|  | static void manageConstructionEvent(color_ostream& out); | ||||||
|  | static void manageSyndromeEvent(color_ostream& out); | ||||||
|  | static void manageInvasionEvent(color_ostream& out); | ||||||
|  | 
 | ||||||
|  | //tick event
 | ||||||
|  | static uint32_t lastTick = 0; | ||||||
|  | 
 | ||||||
|  | //job initiated
 | ||||||
|  | static int32_t lastJobId = -1; | ||||||
|  | 
 | ||||||
|  | //job completed
 | ||||||
|  | static unordered_map<int32_t, df::job*> prevJobs; | ||||||
|  | 
 | ||||||
|  | //unit death
 | ||||||
|  | static unordered_set<int32_t> livingUnits; | ||||||
|  | 
 | ||||||
|  | //item creation
 | ||||||
|  | static int32_t nextItem; | ||||||
|  | 
 | ||||||
|  | //building
 | ||||||
|  | static int32_t nextBuilding; | ||||||
|  | static unordered_set<int32_t> buildings; | ||||||
|  | 
 | ||||||
|  | //construction
 | ||||||
|  | static unordered_set<df::construction*> constructions; | ||||||
|  | static bool gameLoaded; | ||||||
|  | 
 | ||||||
|  | //invasion
 | ||||||
|  | static int32_t nextInvasion; | ||||||
|  | 
 | ||||||
|  | void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) { | ||||||
|  |     static bool doOnce = false; | ||||||
|  |     if ( !doOnce ) { | ||||||
|  |         //TODO: put this somewhere else
 | ||||||
|  |         doOnce = true; | ||||||
|  |         EventHandler buildingHandler(Buildings::updateBuildings, 100); | ||||||
|  |         DFHack::EventManager::registerListener(EventType::BUILDING, buildingHandler, NULL); | ||||||
|  |         //out.print("Registered listeners.\n %d", __LINE__);
 | ||||||
|  |     } | ||||||
|  |     if ( event == DFHack::SC_WORLD_UNLOADED ) { | ||||||
|  |         lastTick = 0; | ||||||
|  |         lastJobId = -1; | ||||||
|  |         for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { | ||||||
|  |             Job::deleteJobStruct((*i).second); | ||||||
|  |         } | ||||||
|  |         prevJobs.clear(); | ||||||
|  |         tickQueue.clear(); | ||||||
|  |         livingUnits.clear(); | ||||||
|  |         nextItem = -1; | ||||||
|  |         nextBuilding = -1; | ||||||
|  |         buildings.clear(); | ||||||
|  |         constructions.clear(); | ||||||
|  | 
 | ||||||
|  |         Buildings::clearBuildings(out); | ||||||
|  |         gameLoaded = false; | ||||||
|  |         nextInvasion = -1; | ||||||
|  |     } else if ( event == DFHack::SC_WORLD_LOADED ) { | ||||||
|  |         uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear | ||||||
|  |             + DFHack::World::ReadCurrentTick(); | ||||||
|  |         multimap<uint32_t,EventHandler> newTickQueue; | ||||||
|  |         for ( auto i = tickQueue.begin(); i != tickQueue.end(); i++ ) { | ||||||
|  |             newTickQueue.insert(pair<uint32_t,EventHandler>(tick + (*i).first, (*i).second)); | ||||||
|  |         } | ||||||
|  |         tickQueue.clear(); | ||||||
|  | 
 | ||||||
|  |         tickQueue.insert(newTickQueue.begin(), newTickQueue.end()); | ||||||
|  | 
 | ||||||
|  |         nextItem = 0; | ||||||
|  |         nextBuilding = 0; | ||||||
|  |         lastTick = 0; | ||||||
|  |         nextInvasion = df::global::ui->invasions.next_id; | ||||||
|  |         gameLoaded = true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void DFHack::EventManager::manageEvents(color_ostream& out) { | ||||||
|  |     if ( !gameLoaded ) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear | ||||||
|  |         + DFHack::World::ReadCurrentTick(); | ||||||
|  |      | ||||||
|  |     if ( tick <= lastTick ) | ||||||
|  |         return; | ||||||
|  |     lastTick = tick; | ||||||
|  | 
 | ||||||
|  |     int32_t eventFrequency[EventType::EVENT_MAX]; | ||||||
|  |     for ( size_t a = 0; a < EventType::EVENT_MAX; a++ ) { | ||||||
|  |         int32_t min = 1000000000; | ||||||
|  |         for ( auto b = handlers[a].begin(); b != handlers[a].end(); b++ ) { | ||||||
|  |             EventHandler bob = (*b).second; | ||||||
|  |             if ( bob.freq < min ) | ||||||
|  |                 min = bob.freq; | ||||||
|  |         } | ||||||
|  |         eventFrequency[a] = min; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     manageTickEvent(out); | ||||||
|  |     if ( tick - eventLastTick[EventType::JOB_INITIATED] >= eventFrequency[EventType::JOB_INITIATED] ) { | ||||||
|  |         manageJobInitiatedEvent(out); | ||||||
|  |         eventLastTick[EventType::JOB_INITIATED] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::JOB_COMPLETED] >= eventFrequency[EventType::JOB_COMPLETED] ) { | ||||||
|  |         manageJobCompletedEvent(out); | ||||||
|  |         eventLastTick[EventType::JOB_COMPLETED] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::UNIT_DEATH] >= eventFrequency[EventType::UNIT_DEATH] ) { | ||||||
|  |         manageUnitDeathEvent(out); | ||||||
|  |         eventLastTick[EventType::UNIT_DEATH] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::ITEM_CREATED] >= eventFrequency[EventType::ITEM_CREATED] ) { | ||||||
|  |         manageItemCreationEvent(out); | ||||||
|  |         eventLastTick[EventType::ITEM_CREATED] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::BUILDING] >= eventFrequency[EventType::BUILDING] ) { | ||||||
|  |         manageBuildingEvent(out); | ||||||
|  |         eventLastTick[EventType::BUILDING] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::CONSTRUCTION] >= eventFrequency[EventType::CONSTRUCTION] ) { | ||||||
|  |         manageConstructionEvent(out); | ||||||
|  |         eventLastTick[EventType::CONSTRUCTION] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::SYNDROME] >= eventFrequency[EventType::SYNDROME] ) { | ||||||
|  |         manageSyndromeEvent(out); | ||||||
|  |         eventLastTick[EventType::SYNDROME] = tick; | ||||||
|  |     } | ||||||
|  |     if ( tick - eventLastTick[EventType::INVASION] >= eventFrequency[EventType::INVASION] ) { | ||||||
|  |         manageInvasionEvent(out); | ||||||
|  |         eventLastTick[EventType::INVASION] = tick; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageTickEvent(color_ostream& out) { | ||||||
|  |     uint32_t tick = DFHack::World::ReadCurrentYear()*ticksPerYear | ||||||
|  |         + DFHack::World::ReadCurrentTick(); | ||||||
|  |     while ( !tickQueue.empty() ) { | ||||||
|  |         if ( tick < (*tickQueue.begin()).first ) | ||||||
|  |             break; | ||||||
|  |         EventHandler handle = (*tickQueue.begin()).second; | ||||||
|  |         tickQueue.erase(tickQueue.begin()); | ||||||
|  |         handle.eventHandler(out, (void*)tick); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageJobInitiatedEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::JOB_INITIATED].empty() ) | ||||||
|  |         return; | ||||||
|  |      | ||||||
|  |     if ( lastJobId == -1 ) { | ||||||
|  |         lastJobId = *df::global::job_next_id - 1; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( lastJobId+1 == *df::global::job_next_id ) { | ||||||
|  |         return; //no new jobs
 | ||||||
|  |     } | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::JOB_INITIATED].begin(), handlers[EventType::JOB_INITIATED].end()); | ||||||
|  |      | ||||||
|  |     for ( df::job_list_link* link = &df::global::world->job_list; link != NULL; link = link->next ) { | ||||||
|  |         if ( link->item == NULL ) | ||||||
|  |             continue; | ||||||
|  |         if ( link->item->id <= lastJobId ) | ||||||
|  |             continue; | ||||||
|  |         for ( auto i = copy.begin(); i != copy.end(); i++ ) { | ||||||
|  |             (*i).second.eventHandler(out, (void*)link->item); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     lastJobId = *df::global::job_next_id - 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageJobCompletedEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::JOB_COMPLETED].empty() ) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end()); | ||||||
|  |     map<int32_t, df::job*> nowJobs; | ||||||
|  |     for ( df::job_list_link* link = &df::global::world->job_list; link != NULL; link = link->next ) { | ||||||
|  |         if ( link->item == NULL ) | ||||||
|  |             continue; | ||||||
|  |         nowJobs[link->item->id] = link->item; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { | ||||||
|  |         if ( nowJobs.find((*i).first) != nowJobs.end() ) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         //recently finished or cancelled job!
 | ||||||
|  |         for ( auto j = copy.begin(); j != copy.end(); j++ ) { | ||||||
|  |             (*j).second.eventHandler(out, (void*)(*i).second); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     //erase old jobs, copy over possibly altered jobs
 | ||||||
|  |     for ( auto i = prevJobs.begin(); i != prevJobs.end(); i++ ) { | ||||||
|  |         Job::deleteJobStruct((*i).second); | ||||||
|  |     } | ||||||
|  |     prevJobs.clear(); | ||||||
|  |      | ||||||
|  |     //create new jobs
 | ||||||
|  |     for ( auto j = nowJobs.begin(); j != nowJobs.end(); j++ ) { | ||||||
|  |         /*map<int32_t, df::job*>::iterator i = prevJobs.find((*j).first);
 | ||||||
|  |         if ( i != prevJobs.end() ) { | ||||||
|  |             continue; | ||||||
|  |         }*/ | ||||||
|  | 
 | ||||||
|  |         df::job* newJob = Job::cloneJobStruct((*j).second, true); | ||||||
|  |         prevJobs[newJob->id] = newJob; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /*//get rid of old pointers to deallocated jobs
 | ||||||
|  |     for ( size_t a = 0; a < toDelete.size(); a++ ) { | ||||||
|  |         prevJobs.erase(a); | ||||||
|  |     }*/ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageUnitDeathEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::UNIT_DEATH].empty() ) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end()); | ||||||
|  |     for ( size_t a = 0; a < df::global::world->units.active.size(); a++ ) { | ||||||
|  |         df::unit* unit = df::global::world->units.active[a]; | ||||||
|  |         if ( unit->counters.death_id == -1 ) { | ||||||
|  |             livingUnits.insert(unit->id); | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         //dead: if dead since last check, trigger events
 | ||||||
|  |         if ( livingUnits.find(unit->id) == livingUnits.end() ) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         for ( auto i = copy.begin(); i != copy.end(); i++ ) { | ||||||
|  |             (*i).second.eventHandler(out, (void*)unit->id); | ||||||
|  |         } | ||||||
|  |         livingUnits.erase(unit->id); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageItemCreationEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::ITEM_CREATED].empty() ) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( nextItem >= *df::global::item_next_id ) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end()); | ||||||
|  |     size_t index = df::item::binsearch_index(df::global::world->items.all, nextItem, false); | ||||||
|  |     for ( size_t a = index; a < df::global::world->items.all.size(); a++ ) { | ||||||
|  |         df::item* item = df::global::world->items.all[a]; | ||||||
|  |         //invaders
 | ||||||
|  |         if ( item->flags.bits.foreign ) | ||||||
|  |             continue; | ||||||
|  |         //traders who bring back your items?
 | ||||||
|  |         if ( item->flags.bits.trader ) | ||||||
|  |             continue; | ||||||
|  |         //migrants
 | ||||||
|  |         if ( item->flags.bits.owned ) | ||||||
|  |             continue; | ||||||
|  |         //spider webs don't count
 | ||||||
|  |         if ( item->flags.bits.spider_web ) | ||||||
|  |             continue; | ||||||
|  |         for ( auto i = copy.begin(); i != copy.end(); i++ ) { | ||||||
|  |             (*i).second.eventHandler(out, (void*)item->id); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     nextItem = *df::global::item_next_id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageBuildingEvent(color_ostream& out) { | ||||||
|  |     /*
 | ||||||
|  |      * TODO: could be faster | ||||||
|  |      * consider looking at jobs: building creation / destruction | ||||||
|  |      **/ | ||||||
|  |     if ( handlers[EventType::BUILDING].empty() ) | ||||||
|  |         return; | ||||||
|  |      | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end()); | ||||||
|  |     //first alert people about new buildings
 | ||||||
|  |     for ( int32_t a = nextBuilding; a < *df::global::building_next_id; a++ ) { | ||||||
|  |         int32_t index = df::building::binsearch_index(df::global::world->buildings.all, a); | ||||||
|  |         if ( index == -1 ) { | ||||||
|  |             //out.print("%s, line %d: Couldn't find new building with id %d.\n", __FILE__, __LINE__, a);
 | ||||||
|  |             //the tricky thing is that when the game first starts, it's ok to skip buildings, but otherwise, if you skip buildings, something is probably wrong. TODO: make this smarter
 | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         buildings.insert(a); | ||||||
|  |         for ( auto b = copy.begin(); b != copy.end(); b++ ) { | ||||||
|  |             EventHandler bob = (*b).second; | ||||||
|  |             bob.eventHandler(out, (void*)a); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     nextBuilding = *df::global::building_next_id; | ||||||
|  |      | ||||||
|  |     //now alert people about destroyed buildings
 | ||||||
|  |     unordered_set<int32_t> toDelete; | ||||||
|  |     for ( auto a = buildings.begin(); a != buildings.end(); a++ ) { | ||||||
|  |         int32_t id = *a; | ||||||
|  |         int32_t index = df::building::binsearch_index(df::global::world->buildings.all,id); | ||||||
|  |         if ( index != -1 ) | ||||||
|  |             continue; | ||||||
|  |         toDelete.insert(id); | ||||||
|  | 
 | ||||||
|  |         for ( auto b = copy.begin(); b != copy.end(); b++ ) { | ||||||
|  |             EventHandler bob = (*b).second; | ||||||
|  |             bob.eventHandler(out, (void*)id); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for ( auto a = toDelete.begin(); a != toDelete.end(); a++ ) { | ||||||
|  |         int32_t id = *a; | ||||||
|  |         buildings.erase(id); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     //out.print("Sent building event.\n %d", __LINE__);
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageConstructionEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::CONSTRUCTION].empty() ) | ||||||
|  |         return; | ||||||
|  | 
 | ||||||
|  |     unordered_set<df::construction*> constructionsNow(df::global::world->constructions.begin(), df::global::world->constructions.end()); | ||||||
|  |      | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end()); | ||||||
|  |     for ( auto a = constructions.begin(); a != constructions.end(); a++ ) { | ||||||
|  |         df::construction* construction = *a; | ||||||
|  |         if ( constructionsNow.find(construction) != constructionsNow.end() ) | ||||||
|  |             continue; | ||||||
|  |         for ( auto b = copy.begin(); b != copy.end(); b++ ) { | ||||||
|  |             EventHandler handle = (*b).second; | ||||||
|  |             handle.eventHandler(out, (void*)construction); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for ( auto a = constructionsNow.begin(); a != constructionsNow.end(); a++ ) { | ||||||
|  |         df::construction* construction = *a; | ||||||
|  |         if ( constructions.find(construction) != constructions.end() ) | ||||||
|  |             continue; | ||||||
|  |         for ( auto b = copy.begin(); b != copy.end(); b++ ) { | ||||||
|  |             EventHandler handle = (*b).second; | ||||||
|  |             handle.eventHandler(out, (void*)construction); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     constructions.clear(); | ||||||
|  |     constructions.insert(constructionsNow.begin(), constructionsNow.end()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageSyndromeEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::SYNDROME].empty() ) | ||||||
|  |         return; | ||||||
|  | 
 | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end()); | ||||||
|  |     for ( auto a = df::global::world->units.active.begin(); a != df::global::world->units.active.end(); a++ ) { | ||||||
|  |         df::unit* unit = *a; | ||||||
|  |         if ( unit->flags1.bits.dead ) | ||||||
|  |             continue; | ||||||
|  |         for ( size_t b = 0; b < unit->syndromes.active.size(); b++ ) { | ||||||
|  |             df::unit_syndrome* syndrome = unit->syndromes.active[b]; | ||||||
|  |             uint32_t startTime = syndrome->year*ticksPerYear + syndrome->year_time; | ||||||
|  |             if ( startTime <= eventLastTick[EventType::SYNDROME] ) | ||||||
|  |                 continue; | ||||||
|  | 
 | ||||||
|  |             SyndromeData data(unit->id, b); | ||||||
|  |             for ( auto c = copy.begin(); c != copy.end(); c++ ) { | ||||||
|  |                 EventHandler handle = (*c).second; | ||||||
|  |                 handle.eventHandler(out, (void*)&data); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static void manageInvasionEvent(color_ostream& out) { | ||||||
|  |     if ( handlers[EventType::INVASION].empty() ) | ||||||
|  |         return; | ||||||
|  | 
 | ||||||
|  |     multimap<Plugin*,EventHandler> copy(handlers[EventType::INVASION].begin(), handlers[EventType::INVASION].end()); | ||||||
|  | 
 | ||||||
|  |     if ( df::global::ui->invasions.next_id <= nextInvasion ) | ||||||
|  |         return; | ||||||
|  |     nextInvasion = df::global::ui->invasions.next_id; | ||||||
|  | 
 | ||||||
|  |     for ( auto a = copy.begin(); a != copy.end(); a++ ) { | ||||||
|  |         EventHandler handle = (*a).second; | ||||||
|  |         handle.eventHandler(out, (void*)nextInvasion); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||