55import Foundation
66#if MAIN_APP
77import AppMakerCore
8+ import LuaSwiftBindings
89#elseif COMPANION
910import AppMakerCompanionCore
1011#endif
@@ -146,11 +147,211 @@ final class Solar2DBuildSystem: AppMakerBuildSystemImplementation {
146147 try ? FileManager . default. removeItem ( at: product. buildProductPath)
147148 let coronaSourceFolder = self . entryPathUrl. deletingLastPathComponent ( )
148149 try ? FileManager . default. createDirectory ( at: product. buildProductPath. deletingLastPathComponent ( ) , withIntermediateDirectories: true )
150+ let mainLuaFile : URL = product. buildProductPath. appendingPathComponent ( " main.lua " )
151+ let configLuaFile : URL = product. buildProductPath. appendingPathComponent ( " config.lua " )
149152 do {
153+ // copy all source file
150154 try FileManager . default. copyItem (
151155 at: coronaSourceFolder,
152156 to: product. buildProductPath
153157 )
158+
159+ let configLuaInfo : ConfigLuaInfo
160+ if FileManager . default. fileExists ( atPath: configLuaFile. path) {
161+ // read config.lua file
162+ func getConfigLuaInfo( ) throws -> ConfigLuaInfo {
163+ let vm = LuaSwiftBindings . VirtualMachine ( )
164+ _ = vm. eval ( try String ( contentsOf: configLuaFile) )
165+ return ConfigLuaInfo ( globalTable: vm. globals)
166+ }
167+ configLuaInfo = try getConfigLuaInfo ( )
168+
169+ // delete given config.lua file
170+ try FileManager . default. removeItem ( at: configLuaFile)
171+
172+ // put in fake config.lua file
173+ try """
174+ application =
175+ {
176+ content =
177+ {
178+ scale = " adaptive " ,
179+ fps = \( configLuaInfo. fps. rawValue) ,
180+ }
181+ }
182+ """ . write ( to: configLuaFile, atomically: true , encoding: . utf8)
183+ } else {
184+ configLuaInfo = ConfigLuaInfo ( )
185+ }
186+
187+
188+ // rename main.lua and create fake main.lua to implement hooks and sandboxing
189+ try FileManager . default. moveItem (
190+ at: mainLuaFile,
191+ to: product. buildProductPath. appendingPathComponent ( " _original_main.lua " )
192+ )
193+
194+ // create fake main.lua
195+ try """
196+ -- encode config.lua information here
197+ local configLuaDetails = \( configLuaInfo. asLuaTableString)
198+
199+ -- call this function to communicate with Solar2DSwiftUIViewDelegate
200+ local function sendToSwift(eventName, args)
201+ Runtime:dispatchEvent( { name= " coronaView " , eventName=eventName, args=args, isForAppMaker=true } )
202+ end
203+
204+ -- send prints to App Maker console
205+ _G.print = function(...)
206+ local args = {...}
207+ local strings = {}
208+ for _, arg in ipairs(args) do
209+ strings[#strings+1] = tostring(arg)
210+ end
211+ sendToSwift( " print " , strings)
212+ end
213+
214+ -- flag ensures that App Maker successfully communicates essential information about
215+ -- where we are running before we continue into the actual main.lua code
216+ local establishedCommunicationWithHostAppMakerProcess = false
217+
218+ -- save original CoronaCard content size values to the config table for App Maker to read
219+ configLuaDetails[ " original_actualContentWidth " ] = display.actualContentWidth
220+ configLuaDetails[ " original_actualContentHeight " ] = display.actualContentHeight
221+
222+ -- fake display object
223+ local displayOverwrites = {}
224+ local oldDisplay = display
225+ display = {}
226+ setmetatable(display, {
227+ __index = function(table, key)
228+ local result = displayOverwrites[key]
229+ if result == nil then
230+ return oldDisplay[key]
231+ else
232+ return result
233+ end
234+ end,
235+ })
236+
237+ -- App Maker will always communicate to the Solar2D CoronaCard view via this listener
238+ local function handleAppMaker( event )
239+ if ( event.eventName == " containingWindowNewSize " ) then
240+ -- todo
241+ -- local imageSuffix = event.imageSuffix
242+
243+ -- standard solar2d screen size values
244+ local actualContentWidth = event.actualContentWidth
245+ local actualContentHeight = event.actualContentHeight
246+ local contentWidth = event.contentWidth
247+ local contentHeight = event.contentHeight
248+ local contentScaleX = event.contentScaleX
249+ local contentScaleY = event.contentScaleY
250+ local pixelWidth = event.pixelWidth
251+ local pixelHeight = event.pixelHeight
252+ local topStatusBarContentHeight = event.topStatusBarContentHeight
253+ local bottomStatusBarContentHeight = event.bottomStatusBarContentHeight
254+
255+ -- simplified representation of some screen properties
256+ local insetT = event.insetT
257+ local insetL = event.insetL
258+ local insetB = event.insetB
259+ local insetR = event.insetR
260+
261+ -- derrived values
262+ local contentCenterX = contentWidth * .5
263+ local contentCenterY = contentHeight * .5
264+ local screenOriginX = (contentWidth - actualContentWidth) * .5
265+ local screenOriginY = (contentHeight - actualContentHeight) * .5
266+ local safeActualContentWidth = actualContentWidth - insetL - insetR
267+ local safeActualContentHeight = actualContentHeight - insetT - insetB
268+ local safeScreenOriginX = screenOriginX + insetL
269+ local safeScreenOriginY = screenOriginY + insetT
270+ local statusBarHeight = topStatusBarContentHeight -- note, this might actually be slightly different from topStatusBarContentHeight, but it doesn't seem worth the time figuring out exactly what that difference is
271+ local getSafeAreaInsets = function()
272+ return insetT, insetL, insetB, insetR
273+ end
274+ local viewableContentWidth = math.min(contentWidth, actualContentWidth)
275+ local viewableContentHeight = math.min(contentHeight, actualContentHeight)
276+
277+ -- hack the global stage display object to fix properly
278+ local _hackOffsetX = event._hackOffsetX
279+ local _hackOffsetY = event._hackOffsetY
280+ local _hackScaleX = event._hackScaleX
281+ local _hackScaleY = event._hackScaleY
282+
283+ -- override values in global display table
284+ displayOverwrites.actualContentWidth = actualContentWidth
285+ displayOverwrites.actualContentHeight = actualContentHeight
286+ displayOverwrites.contentWidth = contentWidth
287+ displayOverwrites.contentHeight = contentHeight
288+ displayOverwrites.contentScaleX = contentScaleX
289+ displayOverwrites.contentScaleY = contentScaleY
290+ displayOverwrites.pixelWidth = pixelWidth
291+ displayOverwrites.pixelHeight = pixelHeight
292+ displayOverwrites.topStatusBarContentHeight = topStatusBarContentHeight
293+ displayOverwrites.bottomStatusBarContentHeight = bottomStatusBarContentHeight
294+ displayOverwrites.viewableContentWidth = viewableContentWidth
295+ displayOverwrites.viewableContentHeight = viewableContentHeight
296+ displayOverwrites.contentCenterX = contentCenterX
297+ displayOverwrites.contentCenterY = contentCenterY
298+ displayOverwrites.safeActualContentWidth = safeActualContentWidth
299+ displayOverwrites.safeActualContentHeight = safeActualContentHeight
300+ displayOverwrites.safeScreenOriginX = safeScreenOriginX
301+ displayOverwrites.safeScreenOriginY = safeScreenOriginY
302+ displayOverwrites.screenOriginX = screenOriginX
303+ displayOverwrites.screenOriginY = screenOriginY
304+ displayOverwrites.statusBarHeight = statusBarHeight
305+ displayOverwrites.getSafeAreaInsets = getSafeAreaInsets
306+
307+ -- apply hack to stage object
308+ oldDisplay.currentStage.x = _hackOffsetX * _hackScaleX
309+ oldDisplay.currentStage.y = _hackOffsetY * _hackScaleY
310+ oldDisplay.currentStage.xScale = _hackScaleX
311+ oldDisplay.currentStage.yScale = _hackScaleY
312+
313+ if establishedCommunicationWithHostAppMakerProcess then
314+ -- not the first time called, call the resize listener
315+ Runtime:dispatchEvent( {name= " resize " } )
316+ else
317+ -- first time called
318+ establishedCommunicationWithHostAppMakerProcess = true
319+ end
320+ end
321+ end
322+ Runtime:addEventListener( " appMaker " , handleAppMaker )
323+
324+ -- intercept addEventListener
325+ local oldAddEventListener = Runtime.addEventListener
326+ function Runtime.addEventListener(...)
327+ local args = {...}
328+ if #args > 1 then
329+ local eventName = args[2]
330+ if eventName == " accelerometer " then
331+ -- the accelerometer is correctly broken for CoronaCards
332+ -- https://forums.solar2d.com/t/gyroscope-and-accelerometer-not-working-on-ios/331006
333+ print( " ⚠️: accelerometer does not work with CoronaCards " )
334+ return
335+ end
336+ end
337+ return oldAddEventListener(unpack(args))
338+ end
339+
340+ -- send embedded config lua details
341+ sendToSwift( " configLuaDetails " , configLuaDetails)
342+
343+ -- actual main.lua
344+ if establishedCommunicationWithHostAppMakerProcess then
345+ local status, err = pcall(function() require( " _original_main " ) end)
346+ if status == false then
347+ sendToSwift( " error " , {message=tostring(err)})
348+ end
349+ else
350+ print( " ❌ failed to establish communication between Solar2D (CoronaCards) and App Maker IDE " )
351+ end
352+ """ . write ( to: mainLuaFile, atomically: true , encoding: . utf8)
353+
354+ // done
154355 return . success( Void ( ) )
155356 } catch {
156357 return . failure( error)
@@ -172,5 +373,98 @@ final class Solar2DBuildSystem: AppMakerBuildSystemImplementation {
172373 #endif
173374 }
174375}
376+
377+
378+ struct ConfigLuaInfo {
379+ let width : Int ?
380+ let height : Int ?
381+ let scale : ScaleMode
382+ let fps : FPS
383+
384+ // for when app is running
385+ let original_actualContentWidth : Double ?
386+ let original_actualContentHeight : Double ?
387+
388+ var desiredSize : CGSize ? {
389+ if let width = width, let height = height {
390+ return CGSize ( width: width, height: height)
391+ }
392+ return nil
393+ }
394+
395+ init ( ) {
396+ self . scale = . none
397+ self . width = nil
398+ self . height = nil
399+ self . fps = . _30
400+ self . original_actualContentWidth = nil
401+ self . original_actualContentHeight = nil
402+ }
403+ init ( globalTable: Table ) {
404+ guard let application: Table = globalTable. get ( key: " application " ) ,
405+ let content: Table = application. get ( key: " content " ) else {
406+ self = . init( )
407+ return
408+ }
409+ self . width = content. get ( key: " width " )
410+ self . height = content. get ( key: " height " )
411+ if let scaleModeStr: String = content. get ( key: " scale " ) {
412+ self . scale = . init( rawValue: scaleModeStr. lowercased ( ) ) ?? . none
413+ } else {
414+ self . scale = . none
415+ }
416+ if let fpsInt: Int = content. get ( key: " fps " ) {
417+ self . fps = . init( rawValue: fpsInt) ?? . _30
418+ } else {
419+ self . fps = . _30
420+ }
421+ self . original_actualContentWidth = nil
422+ self . original_actualContentHeight = nil
423+ }
424+ init ? ( args: Dictionary < AnyHashable , Any > ) {
425+ self . width = ( args [ " width " ] as? NSNumber ) ? . intValue
426+ self . height = ( args [ " height " ] as? NSNumber ) ? . intValue
427+ if let scaleModeStr: String = args [ " scale " ] as? String {
428+ self . scale = . init( rawValue: scaleModeStr. lowercased ( ) ) ?? . none
429+ } else {
430+ self . scale = . none
431+ }
432+ if let fpsInt: NSNumber = args [ " fps " ] as? NSNumber {
433+ self . fps = . init( rawValue: fpsInt. intValue) ?? . _30
434+ } else {
435+ self . fps = . _30
436+ }
437+ guard let original_actualContentWidth: Double = ( args [ " original_actualContentWidth " ] as? NSNumber ) ? . doubleValue,
438+ let original_actualContentHeight: Double = ( args [ " original_actualContentHeight " ] as? NSNumber ) ? . doubleValue else {
439+ return nil
440+ }
441+ self . original_actualContentWidth = original_actualContentWidth
442+ self . original_actualContentHeight = original_actualContentHeight
443+ }
444+
445+ enum ScaleMode : String {
446+ case none = " "
447+ case zoomEven = " zoomeven "
448+ case zoomStretch = " zoomstretch "
449+ case letterbox = " letterbox "
450+ case adaptive = " adaptive "
451+ }
452+
453+ enum FPS : Int {
454+ case _30 = 30
455+ case _60 = 60
456+ }
457+
458+ var asLuaTableString : String {
459+ return """
460+ {
461+ width = \( self . width. asLuaCodeString) ,
462+ height = \( self . height. asLuaCodeString) ,
463+ fps = \( self . fps. rawValue. asLuaCodeString) ,
464+ scale = \( self . scale. rawValue. asLuaCodeString) ,
465+ }
466+ """
467+ }
468+ }
175469#endif
176470
0 commit comments