1 /** @file module.cpp  Action Code Script (ACS) module.
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2005-2015 Daniel Swanson <danij@dengine.net>
5  * @authors Copyright © 1999 Activision
6  *
7  * @par License
8  * GPL: http://www.gnu.org/licenses/gpl.html
9  *
10  * <small>This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by the
12  * Free Software Foundation; either version 2 of the License, or (at your
13  * option) any later version. This program is distributed in the hope that it
14  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
15  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
16  * Public License for more details. You should have received a copy of the GNU
17  * General Public License along with this program; if not, write to the Free
18  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
19  * 02110-1301 USA</small>
20  */
21 
22 #include "common.h"           // IS_CLIENT
23 #include "acs/module.h"
24 
25 #include <QList>
26 #include <QMap>
27 #include <QVector>
28 #include <de/Log>
29 #include "acs/interpreter.h"  // ACS_INTERPRETER_MAX_SCRIPT_ARGS
30 #include "gamesession.h"
31 
32 using namespace de;
33 
34 namespace acs {
35 
DENG2_PIMPL_NOREF(Module)36 DENG2_PIMPL_NOREF(Module)
37 {
38     Block pcode;
39     QVector<EntryPoint> entryPoints;
40     QMap<int, EntryPoint *> epByScriptNumberLut;
41     QList<String> constants;
42 
43     void buildEntryPointLut()
44     {
45         epByScriptNumberLut.clear();
46         for(auto &ep : entryPoints)
47         {
48             epByScriptNumberLut.insert(ep.scriptNumber, &ep);
49         }
50     }
51 };
52 
Module()53 Module::Module() : d(new Impl)
54 {}
55 
recognize(File1 const & file)56 bool Module::recognize(File1 const &file)  // static
57 {
58     if(file.size() <= 4) return false;
59 
60     // ACS bytecode begins with the magic identifier "ACS".
61     Block magic(4);
62     const_cast<File1 &>(file).read(magic.data(), 0, 4);
63     if(!magic.startsWith("ACS")) return false;
64 
65     // ZDoom uses the fourth byte for versioning of their extended formats.
66     // Currently such formats are not supported.
67     return magic.at(3) == 0;
68 }
69 
newFromBytecode(Block const & bytecode)70 Module *Module::newFromBytecode(Block const &bytecode)  // static
71 {
72     DENG2_ASSERT(!IS_CLIENT);
73     LOG_AS("acs::Module");
74 
75     std::unique_ptr<Module> module(new Module);
76 
77     // Copy the complete bytecode data into a local buffer (we'll be randomly
78     // accessing this frequently).
79     module->d->pcode = bytecode;
80 
81     de::Reader from(module->d->pcode);
82     dint32 magic, scriptInfoOffset;
83     from >> magic >> scriptInfoOffset;
84 
85     // Read script entry point info.
86     from.seek(dint(scriptInfoOffset) - dint(from.offset()));
87     dint32 numEntryPoints;
88     from >> numEntryPoints;
89     module->d->entryPoints.reserve(numEntryPoints);
90     for(dint32 i = 0; i < numEntryPoints; ++i)
91     {
92 #define OPEN_SCRIPTS_BASE 1000
93 
94         EntryPoint ep;
95 
96         from >> ep.scriptNumber;
97         if(ep.scriptNumber >= OPEN_SCRIPTS_BASE)
98         {
99             ep.scriptNumber -= OPEN_SCRIPTS_BASE;
100             ep.startWhenMapBegins = true;
101         }
102 
103         dint32 offset;
104         from >> offset;
105         if(offset > dint32(module->d->pcode.size()))
106         {
107             throw FormatError("acs::Module", "Invalid script entrypoint offset");
108         }
109         ep.pcodePtr = (int const *)(module->d->pcode.constData() + offset);
110 
111         from >> ep.scriptArgCount;
112         if(ep.scriptArgCount > ACS_INTERPRETER_MAX_SCRIPT_ARGS)
113         {
114             throw FormatError("acs::Module", "Too many script arguments (" + String::number(ep.scriptArgCount) + " > " + String::number(ACS_INTERPRETER_MAX_SCRIPT_ARGS) + ")");
115         }
116 
117         module->d->entryPoints << ep;  // makes a copy.
118 
119 #undef OPEN_SCRIPTS_BASE
120     }
121     // Prepare a script-number => EntryPoint LUT.
122     module->d->buildEntryPointLut();
123 
124     // Read constant (string-)values.
125     dint32 numConstants;
126     from >> numConstants;
127     QVector<dint32> constantOffsets;
128     constantOffsets.reserve(numConstants);
129     for(dint32 i = 0; i < numConstants; ++i)
130     {
131         dint32 offset;
132         from >> offset;
133         constantOffsets << offset;
134     }
135     for(auto const &offset : constantOffsets)
136     {
137         Block utf;
138         from.setOffset(dsize(offset));
139         from.readUntil(utf, 0);
140         module->d->constants << String::fromUtf8(utf);
141     }
142 
143     return module.release();
144 }
145 
newFromFile(File1 const & file)146 Module *Module::newFromFile(File1 const &file)  // static
147 {
148     DENG2_ASSERT(!IS_CLIENT);
149     LOG_AS("acs::Module");
150     LOG_SCR_VERBOSE("Loading from %s:%s...")
151             << NativePath(file.container().composePath()).pretty()
152             << file.name();
153 
154     // Buffer the whole file.
155     Block buffer(file.size());
156     const_cast<File1 &>(file).read(buffer.data());
157 
158     return newFromBytecode(buffer);
159 }
160 
constant(int stringNumber) const161 String Module::constant(int stringNumber) const
162 {
163     if(stringNumber >= 0 && stringNumber < d->constants.count())
164     {
165         return d->constants[stringNumber];
166     }
167     /// @throw MissingConstantError  Invalid constant (string-)value number specified.
168     throw MissingConstantError("acs::Module::constant", "Unknown constant #" + String::number(stringNumber));
169 }
170 
entryPointCount() const171 int Module::entryPointCount() const
172 {
173     return d->entryPoints.count();
174 }
175 
hasEntryPoint(int scriptNumber) const176 bool Module::hasEntryPoint(int scriptNumber) const
177 {
178     return d->epByScriptNumberLut.contains(scriptNumber);
179 }
180 
entryPoint(int scriptNumber) const181 Module::EntryPoint const &Module::entryPoint(int scriptNumber) const
182 {
183     if(hasEntryPoint(scriptNumber)) return *d->epByScriptNumberLut[scriptNumber];
184     /// @throw MissingEntryPointError  Invalid script number specified.
185     throw MissingEntryPointError("acs::Module::entryPoint", "Unknown script #" + String::number(scriptNumber));
186 }
187 
forAllEntryPoints(std::function<LoopResult (EntryPoint &)> func) const188 LoopResult Module::forAllEntryPoints(std::function<LoopResult (EntryPoint &)> func) const
189 {
190     for(EntryPoint &ep : d->entryPoints)
191     {
192         if(auto result = func(ep)) return result;
193     }
194     return LoopContinue;
195 }
196 
pcode() const197 Block const &Module::pcode() const
198 {
199     return d->pcode;
200 }
201 
202 } // namespace acs
203