variable.cpp

Go to the documentation of this file.
00001 /* $Id: variable.cpp 26694 2008-05-18 13:59:35Z mordante $ */
00002 /*
00003    Copyright (C) 2003 by David White <dave@whitevine.net>
00004    Copyright (C) 2005 - 2008 by Philippe Plantier <ayin@anathas.org>
00005 
00006    Part of the Battle for Wesnoth Project http://www.wesnoth.org/
00007 
00008    This program is free software; you can redistribute it and/or modify
00009    it under the terms of the GNU General Public License version 2
00010    or at your option any later version.
00011    This program is distributed in the hope that it will be useful,
00012    but WITHOUT ANY WARRANTY.
00013 
00014    See the COPYING file for more details.
00015 */
00016 
00017 /**
00018  *  @file variable.cpp
00019  *  Manage WML-variables.
00020  */
00021 
00022 #include "global.hpp"
00023 
00024 #include "gamestatus.hpp"
00025 #include "log.hpp"
00026 
00027 #include <cassert>
00028 #include <iostream>
00029 
00030 #define LOG_NG LOG_STREAM(info, engine)
00031 #define WRN_NG LOG_STREAM(warn, engine)
00032 #define ERR_NG LOG_STREAM(err, engine)
00033 
00034 namespace
00035 {
00036     /** 
00037      * @todo FIXME: the variable repository should be
00038      * a class of variable.hpp, and not the game_state.
00039      */
00040     game_state* repos = NULL;
00041 
00042     // map to track temp storage of inserted tags on the heap
00043     std::map<config const *, int> config_cache;
00044 
00045     // map by hash for equivalent inserted tags already in the cache
00046     std::map<std::string const *, config const *> hash_to_cache;
00047 
00048     // map to remember config hashes that have already been calculated
00049     std::map<config const *, std::string const *> config_hashes;
00050 
00051     config empty_config;
00052 
00053     struct compare_str_ptr {
00054         bool operator()(const std::string* s1, const std::string* s2) const
00055         {
00056             return (*s1) < (*s2);
00057         }
00058     };
00059 
00060     class hash_memory_manager {
00061     public:
00062         const std::string *find(const std::string& str) const {
00063             std::set<std::string const*, compare_str_ptr>::const_iterator itor = mem_.lower_bound(&str);
00064             if(itor == mem_.end() || **itor != str) {
00065                 return NULL;
00066             }
00067             return *itor;
00068         }
00069         void insert(const std::string *newhash) {
00070             mem_.insert(newhash);
00071         }
00072         void clear() {
00073             hash_to_cache.clear();
00074             config_hashes.clear();
00075             std::set<std::string const*, compare_str_ptr>::iterator mem_it,
00076                 mem_end = mem_.end();
00077             for(mem_it = mem_.begin(); mem_it != mem_end; ++mem_it) {
00078                 delete *mem_it;
00079             }
00080             mem_.clear();
00081         }
00082         ~hash_memory_manager() {
00083             clear();
00084         }
00085     private:
00086         std::set<std::string const*, compare_str_ptr> mem_;
00087     };
00088     hash_memory_manager hash_memory;
00089 }
00090 
00091 static const std::string* get_hash_of(const config* cp) {
00092     //first see if the memory of a constant config hash exists
00093     std::map<config const *, std::string const *>::iterator ch_it = config_hashes.find(cp);
00094     if(ch_it != config_hashes.end()) {
00095         return ch_it->second;
00096     }
00097     //next see if an equivalent hash string has been memorized
00098     const std::string & temp_hash = cp->hash();
00099     std::string const* find_hash = hash_memory.find(temp_hash);
00100     if(find_hash != NULL) {
00101         return find_hash;
00102     }
00103     //finally, we just allocate a new hash string to memory
00104     std::string* new_hash = new std::string(temp_hash);
00105     hash_memory.insert(new_hash);
00106     //do not insert into config_hashes (may be a variable config)
00107     return new_hash;
00108 }
00109 
00110 static void increment_config_usage(const config*& key) {
00111     if(key == NULL) return;
00112     std::map<config const *, int>::iterator this_usage =  config_cache.find(key);
00113     if(this_usage != config_cache.end()) {
00114         ++this_usage->second;
00115         return;
00116     }
00117     const std::string *hash = get_hash_of(key);
00118     const config *& cfg_store = hash_to_cache[hash];
00119     if(cfg_store == NULL || (key != cfg_store && *key != *cfg_store)) {
00120         // this is a new volatile config: allocate some memory & update key
00121         key = new config(*key);
00122         // remember this cache to prevent an equivalent one from being created
00123         cfg_store = key;
00124         // since the config is now constant, we can safely memorize the hash
00125         config_hashes[key] = hash;
00126     } else {
00127         // swap the key with an equivalent or equal one in the cache
00128         key = cfg_store;
00129     }
00130     ++(config_cache[key]);
00131 }
00132 
00133 static void decrement_config_usage(const config* key) {
00134     if(key == NULL) return;
00135     std::map<config const *, int>::iterator this_usage = config_cache.find(key);
00136     assert(this_usage != config_cache.end());
00137     if(--(this_usage->second) == 0) {
00138         config_cache.erase(this_usage);
00139         if(config_cache.empty()) {
00140             hash_memory.clear();
00141         } else {
00142             if(!hash_to_cache.empty()) {
00143                 hash_to_cache.erase(get_hash_of(key));
00144             }
00145             config_hashes.erase(key);
00146         }
00147         delete key;
00148     }
00149 }
00150 
00151 vconfig::vconfig() :
00152     cfg_(NULL), cache_key_(NULL)
00153 {
00154 }
00155 
00156 vconfig::vconfig(const config* cfg, const config * cache_key) :
00157     cfg_(cfg), cache_key_(cache_key)
00158 {
00159     increment_config_usage(cache_key_);
00160     if(cache_key_ != cache_key) {
00161         //location of volatile cfg has moved
00162         cfg_ = cache_key_;
00163     }
00164 }
00165 
00166 vconfig::vconfig(const vconfig& v) :
00167     cfg_(v.cfg_), cache_key_(v.cache_key_)
00168 {
00169     increment_config_usage(cache_key_);
00170 }
00171 
00172 vconfig::~vconfig()
00173 {
00174     decrement_config_usage(cache_key_);
00175 }
00176 
00177 vconfig& vconfig::operator=(const vconfig cfg)
00178 {
00179     const config* prev_key = cache_key_;
00180     cfg_ = cfg.cfg_;
00181     cache_key_ = cfg.cache_key_;
00182     increment_config_usage(cache_key_);
00183     decrement_config_usage(prev_key);
00184     return *this;
00185 }
00186 
00187 vconfig& vconfig::operator=(const config* cfg)
00188 {
00189     if(cfg_ != cfg) {
00190         cfg_ = cfg;
00191         decrement_config_usage(cache_key_);
00192         cache_key_ = NULL;
00193     }
00194     return *this;
00195 }
00196 
00197 const config vconfig::get_parsed_config() const
00198 {
00199     config res;
00200 
00201     for(string_map::const_iterator itor = cfg_->values.begin();
00202         itor != cfg_->values.end(); ++itor)
00203     {
00204         res[itor->first] = expand(itor->first);
00205     }
00206 
00207     for(config::all_children_iterator child = cfg_->ordered_begin();
00208         child != cfg_->ordered_end(); ++child)
00209     {
00210         const std::string &child_key = *(*child).first;
00211         if(child_key == "insert_tag") {
00212             vconfig insert_cfg(child->second);
00213             const t_string& name = insert_cfg["name"];
00214             const t_string& vname = insert_cfg["variable"];
00215             if(!recursion_.insert(vname).second) {
00216                 ERR_NG << "vconfig::get_parsed_config() infinite recursion detected, aborting"
00217                     << std::endl;
00218                 res.add_child("insert_tag", insert_cfg.get_config());
00219                 return res;
00220             }
00221             variable_info vinfo(vname, false, variable_info::TYPE_CONTAINER);
00222             if(!vinfo.is_valid) {
00223                 res.add_child(name); //add empty tag
00224             } else if(vinfo.explicit_index) {
00225                 res.add_child(name, vconfig(&(vinfo.as_container())).get_parsed_config());
00226             } else {
00227                 variable_info::array_range range = vinfo.as_array();
00228                 if(range.first == range.second) {
00229                     res.add_child(name); //add empty tag
00230                 }
00231                 while(range.first != range.second) {
00232                     res.add_child(name, vconfig(*range.first++).get_parsed_config());
00233                 }
00234             }
00235             recursion_.erase(vname);
00236         } else {
00237             res.add_child(child_key, vconfig((*child).second).get_parsed_config());
00238         }
00239     }
00240     return res;
00241 }
00242 
00243 vconfig::child_list vconfig::get_children(const std::string& key) const
00244 {
00245     vconfig::child_list res;
00246 
00247     for(config::all_children_iterator child = cfg_->ordered_begin();
00248         child != cfg_->ordered_end(); ++child)
00249     {
00250         const std::string &child_key = *(*child).first;
00251         if(child_key == key) {
00252             res.push_back(vconfig(child->second, cache_key_));
00253         } else if(child_key == "insert_tag") {
00254             vconfig insert_cfg(child->second);
00255             if(insert_cfg["name"] == key) {
00256                 variable_info vinfo(insert_cfg["variable"], false, variable_info::TYPE_CONTAINER);
00257                 if(!vinfo.is_valid) {
00258                     //push back an empty tag
00259                     res.push_back(vconfig(&empty_config));
00260                 } else if(vinfo.explicit_index) {
00261                     config * cp = &(vinfo.as_container());
00262                     res.push_back(vconfig(cp, cp));
00263                 } else {
00264                     variable_info::array_range range = vinfo.as_array();
00265                     if(range.first == range.second) {
00266                         //push back an empty tag
00267                         res.push_back(vconfig(&empty_config));
00268                     }
00269                     while(range.first != range.second) {
00270                         config * cp = *range.first++;
00271                         res.push_back(vconfig(cp, cp));
00272                     }
00273                 }
00274             }
00275         }
00276     }
00277     return res;
00278 }
00279 
00280 vconfig vconfig::child(const std::string& key) const
00281 {
00282     const config *natural = cfg_->child(key);
00283     if(natural)
00284     {
00285         return vconfig(natural, cache_key_);
00286     }
00287     for(config::const_child_itors chitors = cfg_->child_range("insert_tag");
00288         chitors.first != chitors.second; ++chitors.first)
00289     {
00290         vconfig insert_cfg(*chitors.first);
00291         if(insert_cfg["name"] == key) {
00292             variable_info vinfo(insert_cfg["variable"], false, variable_info::TYPE_CONTAINER);
00293             if(!vinfo.is_valid) {
00294                 return vconfig(&empty_config);
00295             }
00296             config * cp = &(vinfo.as_container());
00297             return vconfig(cp, cp);
00298         }
00299     }
00300     return vconfig();
00301 }
00302 
00303 bool vconfig::has_child(const std::string& key) const
00304 {
00305     if(cfg_->child(key) != NULL) {
00306         return true;
00307     }
00308     for(config::const_child_itors chitors = cfg_->child_range("insert_tag");
00309         chitors.first != chitors.second; ++chitors.first)
00310     {
00311         vconfig insert_cfg(*chitors.first);
00312         if(insert_cfg["name"] == key) {
00313             return true;
00314         }
00315     }
00316     return false;
00317 }
00318 
00319 const t_string vconfig::expand(const std::string& key) const
00320 {
00321     const t_string& val = (*cfg_)[key];
00322     if(repos != NULL && !val.str().empty()) {
00323         std::string interp = utils::interpolate_variables_into_string(val.str(), *repos);
00324         if(val.str() != interp) {
00325             return t_string(interp);
00326         }
00327     }
00328     return t_string(val);
00329 }
00330 
00331 vconfig::all_children_iterator::all_children_iterator(config::all_children_iterator i)
00332 : i_(i), inner_index_(0), index_offset_(0)
00333 {
00334 }
00335 
00336 vconfig::all_children_iterator& vconfig::all_children_iterator::operator++()
00337 {
00338     if(i_.get_key() == "insert_tag") {
00339         variable_info vinfo(vconfig(&i_.get_child())["variable"], false, variable_info::TYPE_CONTAINER);
00340         if(vinfo.is_valid && !vinfo.explicit_index) {
00341             variable_info::array_range range = vinfo.as_array();
00342             if(range.first != range.second && range.first + (++inner_index_) != range.second) {
00343                 ++index_offset_;
00344                 return *this;
00345             }
00346         }
00347     }
00348     ++i_;
00349     inner_index_ = 0;
00350     return *this;
00351 }
00352 
00353 vconfig::all_children_iterator vconfig::all_children_iterator::operator++(int)
00354 {
00355     vconfig::all_children_iterator i = *this;
00356     this->operator++();
00357     return i;
00358 }
00359 
00360 std::pair<const std::string,const vconfig> vconfig::all_children_iterator::operator*() const
00361 {
00362     return std::make_pair<const std::string, const vconfig>(get_key(), get_child());
00363 }
00364 
00365 vconfig::all_children_iterator::pointer vconfig::all_children_iterator::operator->() const
00366 {
00367     return pointer(new std::pair<const std::string, const vconfig>(get_key(), get_child()));
00368 }
00369 
00370 const std::string vconfig::all_children_iterator::get_key() const
00371 {
00372     const std::string& key = i_.get_key();
00373     if(key == "insert_tag") {
00374         return vconfig(&i_.get_child())["name"];
00375     }
00376     return key;
00377 }
00378 
00379 const vconfig vconfig::all_children_iterator::get_child() const
00380 {
00381     if(i_.get_key() == "insert_tag") {
00382         config * cp;
00383         variable_info vinfo(vconfig(&i_.get_child())["variable"], false, variable_info::TYPE_CONTAINER);
00384         if(!vinfo.is_valid) {
00385             return vconfig(&empty_config);
00386         } else if(inner_index_ == 0) {
00387             cp = &(vinfo.as_container());
00388             return vconfig(cp, cp);
00389         }
00390         cp = *(vinfo.as_array().first + inner_index_);
00391         return vconfig(cp, cp);
00392     }
00393     return vconfig(&i_.get_child());
00394 }
00395 
00396 size_t vconfig::all_children_iterator::get_index() const
00397 {
00398     return i_.get_index() + index_offset_;
00399 }
00400 
00401 bool vconfig::all_children_iterator::operator==(all_children_iterator i) const
00402 {
00403     return (i_ == i.i_ && inner_index_ == i.inner_index_);
00404 }
00405 
00406 bool vconfig::all_children_iterator::operator!=(all_children_iterator i) const
00407 {
00408     return (i_ != i.i_ || inner_index_ != i.inner_index_);
00409 }
00410 
00411 vconfig::all_children_iterator vconfig::ordered_begin() const
00412 {
00413     return all_children_iterator(cfg_->ordered_begin());
00414 }
00415 
00416 vconfig::all_children_iterator vconfig::ordered_end() const
00417 {
00418     return all_children_iterator(cfg_->ordered_end());
00419 }
00420 
00421 namespace variable
00422 {
00423     manager::manager(game_state* repository)
00424     {
00425         repos = repository;
00426     }
00427 
00428     manager::~manager()
00429     {
00430         repos = NULL;
00431         hash_memory.clear();
00432     }
00433 }
00434 
00435 scoped_wml_variable::scoped_wml_variable(const std::string& var_name)
00436     : var_name_(var_name), activated_(false)
00437 {
00438     repos->scoped_variables.push_back(this);
00439 }
00440 
00441 void scoped_wml_variable::store(const config& var_value)
00442 {
00443     const config::child_list& children = repos->get_variables().get_children(var_name_);
00444     for(config::child_list::const_iterator i = children.begin(); i != children.end(); ++i) {
00445         previous_val_.append(**i);
00446     }
00447     repos->clear_variable_cfg(var_name_);
00448     repos->add_variable_cfg(var_name_, var_value);
00449     activated_ = true;
00450 }
00451 
00452 scoped_wml_variable::~scoped_wml_variable()
00453 {
00454     if(activated_) {
00455         repos->clear_variable_cfg(var_name_);
00456         config::child_list old_val =previous_val_.get_children(var_name_);
00457         for(config::child_list::iterator j=old_val.begin(); j != old_val.end() ; j++){
00458             repos->add_variable_cfg(var_name_,**j);
00459         }
00460     }
00461     assert(repos->scoped_variables.back() == this);
00462     repos->scoped_variables.pop_back();
00463 }
00464 
00465 void scoped_xy_unit::activate()
00466 {
00467     unit_map::const_iterator itor = umap_.find(gamemap::location(x_,y_));
00468     if(itor != umap_.end()) {
00469         config tmp_cfg;
00470         itor->second.write(tmp_cfg);
00471         tmp_cfg["x"] = lexical_cast<std::string,int>(x_ + 1);
00472         tmp_cfg["y"] = lexical_cast<std::string,int>(y_ + 1);
00473         store(tmp_cfg);
00474     } else {
00475         ERR_NG << "failed to auto-store $" << name() << " at (" << x_ << ',' << y_ << ")\n";
00476     }
00477 }
00478 
00479 void scoped_recall_unit::activate()
00480 {
00481     player_info* const player = repos->get_player(player_);
00482     if(player != NULL) {
00483         if(player->available_units.size() > recall_index_) {
00484             config tmp_cfg;
00485             player->available_units[recall_index_].write(tmp_cfg);
00486             tmp_cfg["x"] = "recall";
00487             tmp_cfg["y"] = "recall";
00488             store(tmp_cfg);
00489         } else {
00490             ERR_NG << "failed to auto-store $" << name() << " for player: " << player_
00491                 << " at recall index: " << recall_index_ << '\n';
00492         }
00493     } else {
00494         ERR_NG << "failed to auto-store $" << name() << " for player: " << player_ << '\n';
00495     }
00496 }
00497 
00498 namespace {
00499 bool recursive_activation = false;
00500 
00501 /** Turns on any auto-stored variables */
00502 void activate_scope_variable(std::string var_name)
00503 {
00504     if(recursive_activation)
00505         return;
00506     const std::string::iterator itor = std::find(var_name.begin(),var_name.end(),'.');
00507     if(itor != var_name.end()) {
00508         var_name.erase(itor, var_name.end());
00509     }
00510     std::vector<scoped_wml_variable*>::reverse_iterator rit;
00511     for(rit = repos->scoped_variables.rbegin(); rit != repos->scoped_variables.rend(); ++rit) {
00512         if((**rit).name() == var_name) {
00513             recursive_activation = true;
00514             if(!(**rit).activated()) {
00515                 (**rit).activate();
00516             }
00517             recursive_activation = false;
00518             break;
00519         }
00520     }
00521 }
00522 } // end anonymous namespace
00523 
00524 variable_info::variable_info(const std::string& varname, bool force_valid, TYPE validation_type)
00525     : vartype(validation_type), is_valid(false), explicit_index(false), index(0), vars(NULL)
00526 {
00527     assert(repos != NULL);
00528     activate_scope_variable(varname);
00529 
00530     vars = &repos->variables;
00531     key = varname;
00532     std::string::const_iterator itor = std::find(key.begin(),key.end(),'.');
00533     int dot_index = key.find('.');
00534     // example varname = "unit_store.modifications.trait[0]"
00535     while(itor != key.end()) { // subvar access
00536         std::string element=key.substr(0,dot_index);
00537         key = key.substr(dot_index+1);
00538 
00539         size_t inner_index = 0;
00540         const std::string::iterator index_start = std::find(element.begin(),element.end(),'[');
00541         const bool inner_explicit_index = index_start != element.end();
00542         if(inner_explicit_index) {
00543             const std::string::iterator index_end = std::find(index_start,element.end(),']');
00544             const std::string index_str(index_start+1,index_end);
00545             inner_index = static_cast<size_t>(lexical_cast_default<int>(index_str));
00546             if(inner_index > game_config::max_loop) {
00547                 ERR_NG << "variable_info: index greater than " << game_config::max_loop
00548                        << ", truncated\n";
00549                 inner_index = game_config::max_loop;
00550             }
00551             element = std::string(element.begin(),index_start);
00552         }
00553 
00554         size_t size = vars->get_children(element).size();
00555         if(size <= inner_index) {
00556             if(force_valid) {
00557                 // Add elements to the array until the requested size is attained
00558                 if(inner_explicit_index || key != "length") {
00559                     for(; size <= inner_index; ++size) {
00560                         vars->add_child(element);
00561                     }
00562                 }
00563             } else if(inner_explicit_index) {
00564                 WRN_NG << "variable_info: invalid WML array index, "
00565                     << varname << std::endl;
00566                 return;
00567             } else if(key != "length") {
00568                 WRN_NG << "variable_info: retrieving member of non-existant WML container, "
00569                     << varname << std::endl;
00570                 return;
00571             } //else return length 0 for non-existant WML array (handled below)
00572         }
00573         if(!inner_explicit_index && key == "length") {
00574             switch(vartype) {
00575             case variable_info::TYPE_ARRAY:
00576             case variable_info::TYPE_CONTAINER:
00577                 WRN_NG << "variable_info: using reserved WML variable as wrong type, "
00578                     << varname << std::endl;
00579                 is_valid = force_valid || repos->temporaries.child(varname) != NULL;
00580                 break;
00581             case variable_info::TYPE_SCALAR:
00582             default:
00583                 // Store the length of the array as a temporary variable
00584                 repos->temporaries[varname] = lexical_cast<std::string>(size);
00585                 is_valid = true;
00586                 break;
00587             }
00588             key = varname;
00589             vars = &repos->temporaries;
00590             return;
00591         }
00592 
00593         //std::cerr << "Entering " << element << "[" << inner_index << "] of [" << vars->get_children(element).size() << "]\n";
00594         vars = vars->get_children(element)[inner_index];
00595         itor = std::find(key.begin(),key.end(),'.');
00596         dot_index = key.find('.');
00597     } // end subvar access
00598 
00599     const std::string::iterator index_start = std::find(key.begin(),key.end(),'[');
00600     explicit_index = index_start != key.end();
00601     if(explicit_index) {
00602         const std::string::iterator index_end = std::find(index_start,key.end(),']');
00603         const std::string index_str(index_start+1,index_end);
00604         index = static_cast<size_t>(lexical_cast_default<int>(index_str));
00605         if(index > game_config::max_loop) {
00606             ERR_NG << "variable_info: index greater than " << game_config::max_loop
00607                    << ", truncated\n";
00608             index = game_config::max_loop;
00609         }
00610         key = std::string(key.begin(),index_start);
00611         size_t size = vars->get_children(key).size();
00612         if(size <= index) {
00613             if(!force_valid) {
00614                 WRN_NG << "variable_info: invalid WML array index, " << varname << std::endl;
00615                 return;
00616             }
00617             for(; size <= index; ++size) {
00618                 vars->add_child(key);
00619             }
00620         }
00621         switch(vartype) {
00622         case variable_info::TYPE_ARRAY:
00623             vars = vars->get_children(key)[index];
00624             key = "__array";
00625             is_valid = force_valid || vars->child(key) != NULL;
00626             break;
00627         case variable_info::TYPE_SCALAR:
00628             vars = vars->get_children(key)[index];
00629             key = "__value";
00630             is_valid = force_valid || vars->has_attribute(key);
00631             break;
00632         case variable_info::TYPE_CONTAINER:
00633         case variable_info::TYPE_UNSPECIFIED:
00634         default:
00635             is_valid = true;
00636             return;
00637         }
00638         WRN_NG << "variable_info: using explicitly indexed Container as wrong WML type, "
00639             << varname << std::endl;
00640         explicit_index = false;
00641         index = 0;
00642     } else {
00643         // Final variable is not an explicit index [...]
00644         switch(vartype) {
00645         case variable_info::TYPE_ARRAY:
00646         case variable_info::TYPE_CONTAINER:
00647             is_valid = force_valid || vars->child(key);
00648             break;
00649         case variable_info::TYPE_SCALAR:
00650             is_valid = force_valid || vars->has_attribute(key);
00651             break;
00652         case variable_info::TYPE_UNSPECIFIED:
00653         default:
00654             is_valid = true;
00655             break;
00656         }
00657     }
00658 }
00659 
00660 t_string& variable_info::as_scalar() {
00661     assert(is_valid);
00662     return vars->values[key];
00663 }
00664 
00665 config& variable_info::as_container() {
00666     assert(is_valid);
00667     if(explicit_index) {
00668         // Empty data for explicit index was already created if it was needed
00669         return *vars->get_children(key)[index];
00670     }
00671     config *temp = vars->child(key);
00672     if(temp) {
00673         // The container exists, index not specified, return index 0
00674         return *temp;
00675     }
00676     // Add empty data for the new variable, since it does not exist yet
00677     return vars->add_child(key);
00678 }
00679 
00680 variable_info::array_range variable_info::as_array() {
00681     assert(is_valid);
00682     return vars->child_range(key);
00683 }

Generated by doxygen 1.5.5 on 23 May 2008 for The Battle for Wesnoth
Gna! | Forum | Wiki | CIA | devdocs