1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#include <cstdlib>
19
20#include "bspf.hxx"
21#include "Logger.hxx"
22#include "MediaFactory.hxx"
23#include "Console.hxx"
24#include "Event.hxx"
25#include "EventHandler.hxx"
26#include "FrameBuffer.hxx"
27#include "PropsSet.hxx"
28#include "Sound.hxx"
29#include "Settings.hxx"
30#include "FSNode.hxx"
31#include "OSystem.hxx"
32#include "PNGLibrary.hxx"
33#include "System.hxx"
34#include "TIASurface.hxx"
35#include "ProfilingRunner.hxx"
36
37#include "ThreadDebugging.hxx"
38
39#ifdef DEBUGGER_SUPPORT
40 #include "Debugger.hxx"
41#endif
42
43#ifdef CHEATCODE_SUPPORT
44 #include "CheatManager.hxx"
45#endif
46
47/**
48 Parse the commandline arguments and store into the appropriate hashmap.
49
50 Keys without a corresponding value are assumed to be boolean, and set to true.
51 Some keys are used only by the main function; these are placed in localOpts.
52 The rest are needed globally, and are placed in globalOpts.
53*/
54void parseCommandLine(int ac, char* av[],
55 Settings::Options& globalOpts, Settings::Options& localOpts);
56
57/**
58 Checks the commandline for special settings that are used by various ports
59 to use a specific 'base directory'.
60
61 This needs to be done separately, before either an OSystem or Settings
62 object can be created, since they both depend on each other, and a
63 variable basedir implies a different location for the settings file.
64
65 This function will call OSystem::overrideBaseDir() when either of the
66 applicable arguments are found, and then remove them from the argument
67 list.
68*/
69void checkForCustomBaseDir(Settings::Options& options);
70
71/**
72 Checks whether the commandline contains an argument corresponding to
73 starting a profile session.
74*/
75bool isProfilingRun(int ac, char* av[]);
76
77// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
78void parseCommandLine(int ac, char* av[],
79 Settings::Options& globalOpts, Settings::Options& localOpts)
80{
81 localOpts["ROMFILE"] = ""; // make sure we have an entry for this
82
83 for(int i = 1; i < ac; ++i)
84 {
85 string key = av[i];
86 if(key[0] == '-')
87 {
88 key = key.substr(1);
89
90 // Certain options are used only in the main function
91 if(key == "help" || key == "listrominfo" || key == "rominfo" || key == "takesnapshot")
92 {
93 localOpts[key] = true;
94 continue;
95 }
96 // Take care of arguments without an option that are needed globally
97 if(key == "debug" || key == "holdselect" || key == "holdreset")
98 {
99 globalOpts[key] = true;
100 continue;
101 }
102 // Some ports have the ability to override the base directory where all
103 // configuration files are stored; we check for those next
104 if(key == "baseinappdir")
105 {
106 localOpts[key] = true;
107 continue;
108 }
109
110 if(++i >= ac)
111 {
112 cerr << "Missing argument for '" << key << "'" << endl;
113 continue;
114 }
115 if(key == "basedir" || key == "break")
116 localOpts[key] = av[i];
117 else
118 globalOpts[key] = av[i];
119 }
120 else
121 localOpts["ROMFILE"] = key;
122 }
123
124#if 0
125 cout << "Global opts:" << endl;
126 for(const auto& x: globalOpts)
127 cout << " -> " << x.first << ": " << x.second << endl;
128 cout << "Local opts:" << endl;
129 for(const auto& x: localOpts)
130 cout << " -> " << x.first << ": " << x.second << endl;
131#endif
132}
133
134// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
135void checkForCustomBaseDir(Settings::Options& options)
136{
137 // If both of these are activated, the 'base in app dir' takes precedence
138 auto it = options.find("baseinappdir");
139 if(it != options.end())
140 OSystem::overrideBaseDirWithApp();
141 else
142 {
143 it = options.find("basedir");
144 if(it != options.end())
145 OSystem::overrideBaseDir(it->second.toString());
146 }
147}
148
149// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
150bool isProfilingRun(int ac, char* av[]) {
151 if (ac <= 1) return false;
152
153 return string(av[1]) == "-profile";
154}
155
156// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
157#if defined(BSPF_MACOS)
158int stellaMain(int ac, char* av[])
159#else
160int main(int ac, char* av[])
161#endif
162{
163 SET_MAIN_THREAD;
164
165 std::ios_base::sync_with_stdio(false);
166
167 if (isProfilingRun(ac, av)) {
168 ProfilingRunner runner(ac, av);
169
170 return runner.run() ? 0 : 1;
171 }
172
173 unique_ptr<OSystem> theOSystem;
174
175 auto Cleanup = [&theOSystem]() {
176 if(theOSystem)
177 {
178 Logger::debug("Cleanup from main");
179 theOSystem->saveConfig();
180 theOSystem.reset(); // Force delete of object
181 }
182 MediaFactory::cleanUp(); // Finish any remaining cleanup
183
184 return 0;
185 };
186
187 // Parse the commandline arguments
188 // They are placed in different maps depending on whether they're used
189 // locally or globally
190 Settings::Options globalOpts, localOpts;
191 parseCommandLine(ac, av, globalOpts, localOpts);
192
193 // Check for custom base directory; some ports make use of this
194 checkForCustomBaseDir(localOpts);
195
196 // Create the parent OSystem object and initialize settings
197 theOSystem = MediaFactory::createOSystem();
198 theOSystem->loadConfig(globalOpts);
199
200 // Create the full OSystem after the settings, since settings are
201 // probably needed for defaults
202 Logger::debug("Creating the OSystem ...");
203 if(!theOSystem->create())
204 {
205 Logger::error("ERROR: Couldn't create OSystem");
206 return Cleanup();
207 }
208
209 // Check to see if the user requested info about a specific ROM,
210 // or the list of internal ROMs
211 // If so, show the information and immediately exit
212 string romfile = localOpts["ROMFILE"].toString();
213 if(localOpts["listrominfo"].toBool())
214 {
215 Logger::debug("Showing output from 'listrominfo' ...");
216 theOSystem->propSet().print();
217 return Cleanup();
218 }
219 else if(localOpts["rominfo"].toBool())
220 {
221 Logger::debug("Showing output from 'rominfo' ...");
222 FilesystemNode romnode(romfile);
223 Logger::error(theOSystem->getROMInfo(romnode));
224
225 return Cleanup();
226 }
227 else if(localOpts["help"].toBool())
228 {
229 Logger::debug("Displaying usage");
230 theOSystem->settings().usage();
231 return Cleanup();
232 }
233
234 //// Main loop ////
235 // First we check if a ROM is specified on the commandline. If so, and if
236 // the ROM actually exists, use it to create a new console.
237 // Next we check if a directory is specified on the commandline. If so,
238 // open the rom launcher in that directory.
239 // If not, use the built-in ROM launcher. In this case, we enter 'launcher'
240 // mode and let the main event loop take care of opening a new console/ROM.
241 FilesystemNode romnode(romfile);
242 if(romfile == "" || romnode.isDirectory())
243 {
244 Logger::debug("Attempting to use ROM launcher ...");
245 bool launcherOpened = romfile != "" ?
246 theOSystem->createLauncher(romnode.getPath()) : theOSystem->createLauncher();
247 if(!launcherOpened)
248 {
249 Logger::debug("Launcher could not be started, showing usage");
250 theOSystem->settings().usage();
251 return Cleanup();
252 }
253 }
254 else
255 {
256 try
257 {
258 const string& result = theOSystem->createConsole(romnode);
259 if(result != EmptyString)
260 return Cleanup();
261
262#if 0
263 TODO: Fix this to use functionality from OSystem::mainLoop
264 if(localOpts["takesnapshot"].toBool())
265 {
266 for(int i = 0; i < 30; ++i) theOSystem->frameBuffer().update();
267// theOSystem->frameBuffer().tiaSurface().saveSnapShot();
268 theOSystem->png().takeSnapshot();
269 return Cleanup();
270 }
271#endif
272 }
273 catch(const runtime_error& e)
274 {
275 Logger::error(e.what());
276 return Cleanup();
277 }
278
279#ifdef DEBUGGER_SUPPORT
280 // Set up any breakpoint that was on the command line
281 if(localOpts["break"].toString() != "")
282 {
283 Debugger& dbg = theOSystem->debugger();
284 uInt16 bp = uInt16(dbg.stringToValue(localOpts["break"].toString()));
285 dbg.setBreakPoint(bp);
286 }
287#endif
288 }
289
290 // Start the main loop, and don't exit until the user issues a QUIT command
291 Logger::debug("Starting main loop ...");
292 theOSystem->mainLoop();
293 Logger::debug("Finished main loop ...");
294
295 // Cleanup time ...
296 return Cleanup();
297}
298