mirror of
https://github.com/thug1src/thug.git
synced 2025-01-21 21:33:46 +00:00
1325 lines
41 KiB
C++
1325 lines
41 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// NX.CPP - Platform independent interface to the platfrom specific engine code
|
|
|
|
#include <gfx/nx.h>
|
|
|
|
#include <core/debug.h>
|
|
#include <core/string/stringutils.h>
|
|
|
|
#include <gel/objman.h>
|
|
|
|
#include <gel/scripting/script.h>
|
|
#include <gel/scripting/struct.h>
|
|
#include <gel/scripting/symboltable.h>
|
|
|
|
#include <gfx/nxfont.h>
|
|
#include <gfx/nxmesh.h>
|
|
#include <gfx/nxmodel.h>
|
|
#include <gfx/nxsector.h>
|
|
#include <gfx/nxtexman.h>
|
|
#include <gfx/nxviewman.h>
|
|
#include <gfx/nxwin2d.h>
|
|
#include <gfx/nxmiscfx.h>
|
|
#include <gfx/nxparticlemgr.h>
|
|
#include <gfx/nxquickanim.h>
|
|
#include <gfx/nxlightman.h>
|
|
#include <gfx\nxweather.h>
|
|
#ifdef __PLAT_NGC__
|
|
#include <gfx\ngc\p_nxscene.h>
|
|
#endif // __PLAT_NGC__
|
|
|
|
//#include <sk/modules/skate/skate.h> // For getting list of movable objects
|
|
|
|
#include <sys/replay/replay.h>
|
|
|
|
#ifdef __PLAT_NGPS__
|
|
namespace NxPs2
|
|
{
|
|
extern int geom_stats_total;
|
|
extern int geom_stats_inactive ;
|
|
extern int geom_stats_sky;
|
|
extern int geom_stats_transformed;
|
|
extern int geom_stats_skeletal;
|
|
extern int geom_stats_camera_sphere;
|
|
extern int geom_stats_culled;
|
|
extern int geom_stats_leaf_culled;
|
|
extern int geom_stats_clipcull;
|
|
extern int geom_stats_boxcheck;
|
|
extern int geom_stats_occludecheck;
|
|
extern int geom_stats_occluded;
|
|
extern int geom_stats_colored;
|
|
extern int geom_stats_leaf;
|
|
extern int geom_stats_wibbleUV;
|
|
extern int geom_stats_wibbleVC;
|
|
extern int geom_stats_sendcontext;
|
|
extern int geom_stats_sorted;
|
|
extern int geom_stats_shadow;
|
|
|
|
extern bool DoLetterbox;
|
|
}
|
|
#endif
|
|
|
|
namespace Nx
|
|
{
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// Static definitions
|
|
|
|
CScene * CEngine::sp_loaded_scenes[CEngine::MAX_LOADED_SCENES];
|
|
Lst::HashTable< CParticle >* CEngine::p_particle_table = NULL;
|
|
uint32 CEngine::s_next_avail_sector_checksum = 1; // 0 is invalid
|
|
uint32 CEngine::s_render_mode;
|
|
uint32 CEngine::s_wireframe_mode;
|
|
uint32 CEngine::s_screen_blur;
|
|
uint32 CEngine::s_debug_ignore_1;
|
|
uint32 CEngine::s_debug_ignore_0;
|
|
Obj::CGeneralManager* CEngine::sp_moving_object_manager;
|
|
CImposterManager* CEngine::mp_imposter_manager = NULL;
|
|
CNewParticleManager* CEngine::mp_particle_manager = NULL;
|
|
CWeather* CEngine::mp_weather = NULL;
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* get a sector pointer, based on the checksum of the name */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CSector * CEngine::sGetSector(uint32 sector_checksum)
|
|
{
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
CSector *ret_sector = sp_loaded_scenes[i]->GetSector(sector_checksum);
|
|
if (ret_sector) return ret_sector;
|
|
}
|
|
}
|
|
|
|
// If we got here, we didn't find anything
|
|
return NULL;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sStartEngine()
|
|
{
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
Dbg_Assert(!sp_loaded_scenes[i]); // Make sure it was deallocated
|
|
}
|
|
|
|
// Allocate CTexts and CWindow2Ds
|
|
Nx::CTextMan::sAllocTextPool();
|
|
Nx::CWindow2DManager::sAllocWindow2DPool();
|
|
|
|
CViewportManager::sSetScreenAspect(4.0f/3.0f);
|
|
CViewportManager::sSetDefaultAngle( Script::GetFloat("camera_fov") );
|
|
CViewportManager::sSetScreenAngle( 0.0f); // set to default
|
|
|
|
CViewportManager::sInit();
|
|
CViewportManager::sSetScreenMode(vONE_CAM);
|
|
|
|
//sSetMaxMultipassDistance(3000.0f);
|
|
|
|
// Allocate particle system table
|
|
p_particle_table = new Lst::HashTable<CParticle>( 6 );
|
|
|
|
// Create the imposter manager.
|
|
mp_imposter_manager = new CImposterManager;
|
|
|
|
// Startup effects allocations.
|
|
Nx::MiscFXInitialize();
|
|
|
|
s_plat_start_engine();
|
|
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sSuspendEngine()
|
|
{
|
|
printf ("STUB: SuspendEngine\n");
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sResumeEngine()
|
|
{
|
|
printf ("STUB: ResumeEngine\n");
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sPreRender()
|
|
{
|
|
s_plat_pre_render();
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sPostRender()
|
|
{
|
|
s_plat_post_render();
|
|
|
|
// Clear away any old cameras
|
|
CViewportManager::sDeleteMarkedCameras();
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sRenderWorld()
|
|
{
|
|
process_particles( Tmr::FrameLength() );
|
|
|
|
ScreenFlashUpdate();
|
|
TextureSplatUpdate();
|
|
ShatterUpdate();
|
|
|
|
CLightManager::sUpdateVCLights();
|
|
|
|
s_plat_render_world();
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CScene * CEngine::sCreateScene(const char *p_name, CTexDict *p_tex_dict,
|
|
bool add_super_sectors)
|
|
{
|
|
CScene *p_created_scene = s_plat_create_scene(p_name, p_tex_dict, add_super_sectors);
|
|
|
|
p_created_scene->SetID(Script::GenerateCRC(p_name)); // store the checksum of the scene name
|
|
|
|
p_created_scene->SetTexDict(p_tex_dict);
|
|
|
|
// Put in scene array
|
|
int i;
|
|
for (i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (!sp_loaded_scenes[i])
|
|
{
|
|
sp_loaded_scenes[i] = p_created_scene;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Dbg_MsgAssert(i < MAX_LOADED_SCENES, ("Have more than MAX_LOADED SCENES"));
|
|
|
|
return p_created_scene;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CScene * CEngine::sLoadScene(const char *p_name, CTexDict *p_tex_dict,
|
|
bool add_super_sectors, bool is_sky, bool is_dictionary, bool is_net)
|
|
{
|
|
// first expand the scene file name out to include the path
|
|
// the level directory, and the platform specific extension
|
|
// e.g., "can" become levels\can\can.scn.ps2
|
|
|
|
|
|
|
|
char full_name[128];
|
|
#ifdef __PLAT_NGPS__
|
|
sprintf(full_name,"levels\\%s\\%s%s.geom.%s",p_name,p_name,is_net?"_net":"",sGetPlatformExtension());
|
|
#else
|
|
sprintf(full_name,"levels\\%s\\%s%s.scn.%s", p_name,p_name,is_net?"_net":"",sGetPlatformExtension());
|
|
#endif
|
|
|
|
Mem::PushMemProfile((char*)full_name);
|
|
|
|
CScene *loaded_scene = s_plat_load_scene(full_name, p_tex_dict, add_super_sectors, is_sky, is_dictionary);
|
|
|
|
loaded_scene->SetID(Script::GenerateCRC(p_name)); // store the checksum of the scene name
|
|
|
|
loaded_scene->SetTexDict(p_tex_dict);
|
|
|
|
// Do post-load processing
|
|
loaded_scene->PostLoad(full_name);
|
|
|
|
// Put in scene array
|
|
int i;
|
|
for (i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (!sp_loaded_scenes[i])
|
|
{
|
|
sp_loaded_scenes[i] = loaded_scene;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Dbg_MsgAssert(i < MAX_LOADED_SCENES, ("Have more than MAX_LOADED SCENES"));
|
|
|
|
Mem::PopMemProfile(/*p_name*/);
|
|
|
|
return loaded_scene;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CScene * CEngine::sAddScene(const char *p_scene_name, const char *p_filename)
|
|
{
|
|
// first expand the scene file name out to include the path
|
|
// the level directory, and the platform specific extension
|
|
// e.g., "can" become levels\can\can.scn.ps2
|
|
|
|
char full_name[128];
|
|
#ifdef __PLAT_NGPS__
|
|
sprintf(full_name,"levels\\%s\\%s.geom.%s",p_filename,p_filename,sGetPlatformExtension());
|
|
#else
|
|
sprintf(full_name,"levels\\%s\\%s.scn.%s",p_filename,p_filename,sGetPlatformExtension());
|
|
#endif
|
|
|
|
char texture_dict_name[128];
|
|
sprintf(texture_dict_name,"levels\\%s\\%s.tex",p_filename,p_filename);
|
|
|
|
CScene *p_scene = sGetScene(Script::GenerateCRC(p_scene_name));
|
|
|
|
Dbg_MsgAssert(p_scene, ("sAddScene(): Can't find existing scene %s", p_scene_name));
|
|
|
|
Dbg_Message("Adding to scene %s with file %s", p_scene_name, full_name);
|
|
|
|
// Check if we need to unload old one
|
|
if (strcmp(p_scene->GetAddSceneFilename(), full_name) == 0)
|
|
{
|
|
Dbg_Message("Unloading old added scene %s", full_name);
|
|
p_scene->UnloadAddScene();
|
|
|
|
Nx::CTexDict * p_old_tex_dict = Nx::CTexDictManager::sGetTextureDictionary(Crc::GenerateCRCFromString(texture_dict_name));
|
|
if (p_old_tex_dict) // See if we previously loaded this
|
|
{
|
|
Dbg_Message("Unloading old texture dictionary %s", texture_dict_name);
|
|
Nx::CTexDictManager::sUnloadTextureDictionary(p_old_tex_dict);
|
|
} else {
|
|
Dbg_Assert(0);
|
|
}
|
|
}
|
|
|
|
Nx::CTexDict * p_tex_dict = Nx::CTexDictManager::sLoadTextureDictionary(texture_dict_name,true);
|
|
Dbg_MsgAssert(p_tex_dict, ("ERROR loading tex dict for %s\n",texture_dict_name));
|
|
|
|
Dbg_Message("Adding to scene %s with file %s", p_scene_name, full_name);
|
|
|
|
s_plat_add_scene(p_scene, full_name);
|
|
|
|
// Do post-add processing
|
|
p_scene->PostAdd(full_name, p_tex_dict);
|
|
|
|
return p_scene;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sToggleAddScenes()
|
|
{
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
sp_loaded_scenes[i]->ToggleAddScene();
|
|
}
|
|
}
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CEngine::sUnloadScene(CScene *p_scene)
|
|
{
|
|
|
|
// Wait for the frame to finish rendering before we unload the scene
|
|
// otherwise we might be trying to render something that is no longer there
|
|
// (Without this line, the engine can hang when doing a quickview, and seems
|
|
// to be more crashy when using particle systems)
|
|
Nx::CEngine::sFinishRendering();
|
|
|
|
// Remove Incremental Data first
|
|
// (Doesn't work yet because the replace CSectors are lost and the texture dictionary is still around)
|
|
p_scene->UnloadAddScene();
|
|
|
|
// remove platform specific assets
|
|
if (s_plat_unload_scene(p_scene))
|
|
{
|
|
//#ifdef __PLAT_NGC__
|
|
// CNgcScene *p_ngc_scene = (CNgcScene*)p_scene;
|
|
// NxNgc::sScene * p_engine_scene = p_ngc_scene->GetEngineScene();
|
|
//#endif // __PLAT_NGC__
|
|
|
|
// delete the scene object itself.
|
|
delete p_scene;
|
|
|
|
//#ifdef __PLAT_NGC__
|
|
// // Get the engine specific scene data and pass it to the engine to delete.
|
|
// NxNgc::DeleteScene( p_engine_scene );
|
|
// p_ngc_scene->SetEngineScene( NULL );
|
|
//#endif // __PLAT_NGC__
|
|
|
|
// and remove it from the list of scenes
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i] == p_scene)
|
|
{
|
|
sp_loaded_scenes[i] = NULL;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Dbg_Error("Could not find CScene to unload");
|
|
return false;
|
|
}
|
|
|
|
|
|
// (Not working yet!!!)
|
|
// This is useful function to be only used during development
|
|
// it attempts to reload just the .scn part of a level
|
|
// without reloading the texture dictionary (assumes it stays the same)
|
|
// and without reloading the collision (assumes you are in viewer mode, and don't need it)
|
|
bool CEngine::sQuickReloadGeometry(const char *p_scene_name)
|
|
{
|
|
// find the scene
|
|
Nx::CScene *p_scene = sGetScene(p_scene_name);
|
|
|
|
if (p_scene)
|
|
{
|
|
CTexDict *p_tex_dict = p_scene->GetTexDict();
|
|
bool is_sky = p_scene->IsSky();
|
|
|
|
sUnloadScene(p_scene);
|
|
/*Nx::CScene *p_new_scene = */ sLoadScene(p_scene_name,p_tex_dict,!is_sky,is_sky);
|
|
// sUnloadScene(p_scene);
|
|
// p_new_scene = sLoadScene(p_scene_name,p_tex_dict,!is_sky,is_sky);
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Given the id of a scene, then return the CScene that has this id
|
|
// returns NULL if scene not found
|
|
CScene * CEngine::sGetScene(uint32 id)
|
|
{
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i] && (sp_loaded_scenes[i]->GetID() == id))
|
|
{
|
|
return sp_loaded_scenes[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
CScene * CEngine::sGetScene(const char *p_name)
|
|
{
|
|
return sGetScene(Script::GenerateCRC(p_name));
|
|
}
|
|
|
|
|
|
// Get the last non-sky scene. Kind of application dependent, as it assumes
|
|
// that the "Main" scene is the last non sky scene
|
|
// but that's a pretty safe bet for what we are doing now.
|
|
// (Mick: I'm only using this for the "Disco" cheat in THPS4)
|
|
// (Mick: now this function is used in ParseNodeArray, so I had to change this from
|
|
// using the first non-sky to the last non-sky, so now it's even dodgier....
|
|
// we need a better way of saying what is the "main" scene
|
|
CScene * CEngine::sGetMainScene()
|
|
{
|
|
for (int i = MAX_LOADED_SCENES-1; i >= 0; i--)
|
|
{
|
|
if (sp_loaded_scenes[i] && !sp_loaded_scenes[i]->IsSky())
|
|
{
|
|
return sp_loaded_scenes[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Get the first sky scene.
|
|
CScene * CEngine::sGetSkyScene()
|
|
{
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i] && sp_loaded_scenes[i]->IsSky())
|
|
{
|
|
return sp_loaded_scenes[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// Mick::::: ........
|
|
// a temporary utility function
|
|
// unloads all loaded scenes, (level and sky)
|
|
// and also unloads the texture dictionaries
|
|
// (or at least decrements the reference counts)
|
|
bool CEngine::sUnloadAllScenesAndTexDicts()
|
|
{
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
CScene * p_scene = sp_loaded_scenes[i];
|
|
if (p_scene)
|
|
{
|
|
CTexDict *p_tex_dict = p_scene->GetTexDict();
|
|
|
|
sUnloadScene(p_scene);
|
|
// must unload dictionary after scene
|
|
if (p_tex_dict)
|
|
{
|
|
CTexDictManager::sUnloadTextureDictionary(p_tex_dict);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove occlusion polys.
|
|
s_plat_remove_all_occlusion_polys();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CSprite * CEngine::sCreateSprite(CWindow2D *p_window)
|
|
{
|
|
return s_plat_create_sprite(p_window);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CEngine::sDestroySprite(CSprite *p_sprite)
|
|
{
|
|
return s_plat_destroy_sprite(p_sprite);
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CTextured3dPoly *CEngine::sCreateTextured3dPoly()
|
|
{
|
|
return s_plat_create_textured_3d_poly();
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CEngine::sDestroyTextured3dPoly(CTextured3dPoly *p_poly)
|
|
{
|
|
return s_plat_destroy_textured_3d_poly(p_poly);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CTexture *CEngine::sCreateRenderTargetTexture( int width, int height, int depth, int z_depth )
|
|
{
|
|
return s_plat_create_render_target_texture( width, height, depth, z_depth );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sProjectTextureIntoScene( Nx::CTexture *p_texture, Nx::CModel *p_model, Nx::CScene *p_scene )
|
|
{
|
|
// usually we don't want to know the scene, so we pass in NULL
|
|
// and let the engine figure it out
|
|
// if we have multiple scenes, then we would need to figure this out externally
|
|
|
|
if (!p_scene)
|
|
{
|
|
// find the first non-sky scene
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i] && !sp_loaded_scenes[i]->IsSky())
|
|
{
|
|
p_scene = sp_loaded_scenes[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// It no longer matters if the scene is NULL.
|
|
s_plat_project_texture_into_scene( p_texture, p_model, p_scene );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sSetProjectionTextureCamera( Nx::CTexture *p_texture, Gfx::Camera *p_camera )
|
|
{
|
|
s_plat_set_projection_texture_camera( p_texture, p_camera );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
void CEngine::sStopProjectionTexture( Nx::CTexture *p_texture )
|
|
{
|
|
s_plat_stop_projection_texture( p_texture );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
void CEngine::sAddOcclusionPoly( uint32 num_verts, Mth::Vector *p_vert_array, uint32 checksum )
|
|
{
|
|
s_plat_add_occlusion_poly( num_verts, p_vert_array, checksum );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
void CEngine::sEnableOcclusionPoly( uint32 checksum, bool enable )
|
|
{
|
|
s_plat_enable_occlusion_poly( checksum, enable );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
void CEngine::sDebugCheckForHoles()
|
|
{
|
|
|
|
Tmr::Time start_time, end_time;
|
|
start_time = Tmr::GetTime();
|
|
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i] && !sp_loaded_scenes[i]->IsSky())
|
|
{
|
|
sp_loaded_scenes[i]->DebugCheckForHoles();
|
|
}
|
|
}
|
|
|
|
end_time = Tmr::GetTime();
|
|
|
|
Dbg_Message("CheckForHoles() took %d msecs", end_time - start_time);
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CModel* CEngine::sInitModel(void)
|
|
{
|
|
return s_plat_init_model();
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CEngine::sUninitModel(CModel* pModel)
|
|
{
|
|
// Wait for any async rendering to finish before we proceed
|
|
#ifndef __PLAT_NGC__
|
|
sFinishRendering();
|
|
#endif // __PLAT_NGC__
|
|
return s_plat_uninit_model(pModel);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CGeom* CEngine::sInitGeom()
|
|
{
|
|
return s_plat_init_geom();
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CEngine::sUninitGeom(CGeom* pGeom)
|
|
{
|
|
// Wait for any async rendering to finish before we proceed
|
|
#ifndef __PLAT_NGC__
|
|
sFinishRendering();
|
|
#endif // __PLAT_NGC__
|
|
return s_plat_uninit_geom(pGeom);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CQuickAnim* CEngine::sInitQuickAnim(void)
|
|
{
|
|
return s_plat_init_quick_anim();
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sUninitQuickAnim(CQuickAnim* pQuickAnim)
|
|
{
|
|
s_plat_uninit_quick_anim(pQuickAnim);
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CMesh* CEngine::sLoadMesh(const char* pFileName, uint32 texDictOffset, bool forceTexDictLookup, bool doShadowVolume)
|
|
{
|
|
char baseName[512];
|
|
strcpy( baseName, pFileName );
|
|
|
|
char* pEnd;
|
|
|
|
bool foundSkin = false;
|
|
pEnd = strstr( baseName, ".skin" );
|
|
foundSkin = ( pEnd != NULL );
|
|
if ( foundSkin )
|
|
{
|
|
*pEnd = NULL;
|
|
}
|
|
|
|
bool foundModel = false;
|
|
pEnd = strstr( baseName, ".mdl" );
|
|
foundModel = ( pEnd != NULL );
|
|
if ( foundModel )
|
|
{
|
|
*pEnd = NULL;
|
|
}
|
|
|
|
Dbg_Assert( foundSkin || foundModel );
|
|
|
|
char meshName[512];
|
|
Str::LowerCase( meshName );
|
|
sprintf( meshName, "%s.%s.%s", baseName, foundSkin ? "skin" : "mdl", sGetPlatformExtension() );
|
|
|
|
char textureName[512];
|
|
sprintf( textureName, "%s.tex", baseName );
|
|
|
|
// brad - special case for boardshop boards.
|
|
// (ideally, this would be coming in from a
|
|
// higher level, such as with the
|
|
// "forceTexDictLookup" flag
|
|
if ( strstr( meshName, "thps4board_01" ) )
|
|
{
|
|
forceTexDictLookup = true;
|
|
}
|
|
|
|
Nx::CTexDict* pTexDict = Nx::CTexDictManager::sLoadTextureDictionary(textureName, false, texDictOffset, foundSkin, forceTexDictLookup);
|
|
Dbg_MsgAssert(pTexDict,("ERROR loading tex dict for %s\n",textureName));
|
|
|
|
Mem::PushMemProfile((char*)meshName);
|
|
CMesh* p_mesh = s_plat_load_mesh(meshName, pTexDict, texDictOffset, foundSkin, doShadowVolume);
|
|
Mem::PopMemProfile(/*(char*)meshname*/);
|
|
return p_mesh;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
CMesh* CEngine::sLoadMesh(uint32 id, uint32* pModelData, int modelDataSize, uint8* pCASData, uint32 textureDictChecksum, uint32* pTextureData, int textureDataSize, uint32 textureDictOffset, bool isSkin, bool doShadowVolume )
|
|
{
|
|
Dbg_MsgAssert( id != 0, ( "invalid id checksum" ) );
|
|
|
|
bool forceTexDictLookup = false;
|
|
|
|
Nx::CTexDict* pTexDict = Nx::CTexDictManager::sLoadTextureDictionary(textureDictChecksum, pTextureData, textureDataSize, false, textureDictOffset, isSkin, forceTexDictLookup);
|
|
Dbg_MsgAssert(pTexDict,("ERROR loading tex dict from buffer"));
|
|
|
|
Mem::PushMemProfile((char*)"model_data_buffer");
|
|
CMesh* p_mesh = s_plat_load_mesh(id, pModelData, modelDataSize, pCASData, pTexDict, textureDictOffset, isSkin, doShadowVolume);
|
|
Mem::PopMemProfile(/*(char*)meshname*/);
|
|
return p_mesh;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
bool CEngine::sUnloadMesh(CMesh* pMesh)
|
|
{
|
|
CTexDict *p_tex_dict = pMesh->GetTextureDictionary();
|
|
|
|
bool result = s_plat_unload_mesh(pMesh);
|
|
|
|
CTexDictManager::sUnloadTextureDictionary( p_tex_dict );
|
|
|
|
return result;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
void CEngine::sSetMeshScalingParameters( SMeshScalingParameters* pParams )
|
|
{
|
|
s_plat_set_mesh_scaling_parameters( pParams );
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
// Set the moving object manager. Perviously we asked the skater module for
|
|
// this, but now we are independent of Skate, so we have to be told
|
|
|
|
void CEngine::sSetMovableObjectManager(Obj::CGeneralManager* p_object_manager)
|
|
{
|
|
Dbg_MsgAssert(p_object_manager,("NULL Movable Object Manager"));
|
|
sp_moving_object_manager = p_object_manager;
|
|
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
Lst::Head<Obj::CObject> & CEngine::sGetMovableObjects()
|
|
{
|
|
|
|
Dbg_MsgAssert( sp_moving_object_manager, ("NULL sp_moving_object_manager"));
|
|
return sp_moving_object_manager->GetRefObjectList();
|
|
}
|
|
|
|
Lst::HashTable< CParticle > * CEngine::sGetParticleTable()
|
|
{
|
|
return p_particle_table;
|
|
}
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
SSec::Manager * CEngine::sGetNearestSuperSectorManager(Mth::Line &collision_line)
|
|
{
|
|
// For now, just return the first manager
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
SSec::Manager *p_man = sp_loaded_scenes[i]->GetSuperSectorManager();
|
|
if (p_man) return p_man;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
SSec::Manager * CEngine::sGetNearestSuperSectorManager(Mth::Vector &collision_point)
|
|
{
|
|
// For now, just return the first manager
|
|
for (int i = 0; i < MAX_LOADED_SCENES; i++)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
SSec::Manager *p_man = sp_loaded_scenes[i]->GetSuperSectorManager();
|
|
if (p_man) return p_man;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
uint32 CEngine::sGetUnusedSectorChecksum()
|
|
{
|
|
// Make sure this checksum isn't being used
|
|
while (sGetSector(s_next_avail_sector_checksum))
|
|
s_next_avail_sector_checksum++;
|
|
|
|
return s_next_avail_sector_checksum++;
|
|
}
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* return platform specifc extension, like ".PS2" */
|
|
/* */
|
|
/******************************************************************/
|
|
|
|
const char * CEngine::sGetPlatformExtension()
|
|
{
|
|
return s_plat_get_platform_extension();
|
|
}
|
|
|
|
/******************************************************************/
|
|
// Wait for any pending asyncronous rendering to finish, so rendering
|
|
// data can be unloaded
|
|
// (Might be a wait, or a flush, depending on platform)
|
|
// This function should generally return immediatly if there is no
|
|
// pending rendering. However, it might stall for up to a frame if
|
|
// there are things waiting to render.
|
|
// so if it is to be used in a real-time situation, then you want to
|
|
// call it as late as possible in the frame (preferably the last thing)
|
|
// so it incurs as little wait as possible
|
|
// this might be accomplished by making the "task" that uses it
|
|
// be as low priority as possible
|
|
/******************************************************************/
|
|
|
|
void CEngine::sFinishRendering()
|
|
{
|
|
s_plat_finish_rendering();
|
|
}
|
|
|
|
|
|
void CEngine::sToggleRenderMode()
|
|
{
|
|
s_render_mode++;
|
|
if (s_render_mode == 4)
|
|
{
|
|
s_render_mode = 0;
|
|
}
|
|
}
|
|
|
|
|
|
void CEngine::sSetDebugIgnore(uint32 ignore_1, uint32 ignore_0)
|
|
{
|
|
s_debug_ignore_0 = ignore_0;
|
|
s_debug_ignore_1 = ignore_1;
|
|
}
|
|
|
|
bool CEngine::sIsVisible(Mth::Vector ¢er, float radius)
|
|
{
|
|
// PATCH for multiple viewports
|
|
// always return true, until we sort out the per-viewport visibility (which might not be needed)
|
|
if (Nx::CViewportManager::sGetNumActiveViewports() > 1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return s_plat_is_visible(center,radius);
|
|
}
|
|
|
|
void CEngine::sSetMaxMultipassDistance(float dist)
|
|
{
|
|
s_plat_set_max_multipass_distance(dist);
|
|
}
|
|
|
|
void CEngine::sSetScreenBlur(uint32 amount)
|
|
{
|
|
Dbg_MsgAssert(amount >=0 && amount <=255,("Screen blur amount out of range of 0..255 (it's %d)",amount));
|
|
s_screen_blur = amount;
|
|
s_plat_set_screen_blur(amount);
|
|
}
|
|
|
|
uint32 CEngine::sGetScreenBlur()
|
|
{
|
|
uint32 amount = s_screen_blur;
|
|
Dbg_MsgAssert(amount >=0 && amount <=255,("s_screen_blur out of range of 0..255 (it's %d), maybe corrupted???",amount));
|
|
return amount;
|
|
}
|
|
|
|
int CEngine::sGetNumSoundtracks()
|
|
{
|
|
return s_plat_get_num_soundtracks();
|
|
}
|
|
|
|
const char* CEngine::sGetSoundtrackName( int soundtrack_number )
|
|
{
|
|
const char* p_soundtrack_name_wide = s_plat_get_soundtrack_name( soundtrack_number );
|
|
return p_soundtrack_name_wide;
|
|
}
|
|
|
|
// Added by Ken for use by replay code.
|
|
//
|
|
int CEngine::sWriteSectorStatusBitfield(uint32 *p_bitfield, int numUint32s)
|
|
{
|
|
return 0;
|
|
|
|
// Removed since we no longer have saving of replays to mem card.
|
|
// Leaving this code in was causing a problem in that the passed numUint32s
|
|
// was too small for the Skillzilla Park. Rather than increasing numUint32's
|
|
// and hence using up more memory, it was safer just to remove the contents of this function.
|
|
#if 0
|
|
Dbg_MsgAssert(p_bitfield,("NULL p_bitfield"));
|
|
|
|
int i;
|
|
for (i=0; i<numUint32s; ++i)
|
|
{
|
|
p_bitfield[i]=0;
|
|
}
|
|
|
|
int long_index=0;
|
|
int bit_index=0;
|
|
|
|
for (i = 0; i<MAX_LOADED_SCENES; ++i)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
Lst::HashTable<CSector> *p_table=sp_loaded_scenes[i]->GetSectorList();
|
|
Dbg_MsgAssert(p_table,("NULL p_table"));
|
|
|
|
p_table->IterateStart();
|
|
CSector *p_sector=p_table->IterateNext();
|
|
while (p_sector)
|
|
{
|
|
if (p_sector->GetActiveAtReplayStart())
|
|
{
|
|
Dbg_MsgAssert(long_index<numUint32s,("Too many sectors for bitfield !"));
|
|
p_bitfield[long_index] |= (1<<bit_index);
|
|
}
|
|
|
|
++bit_index;
|
|
if (bit_index>=32)
|
|
{
|
|
bit_index=0;
|
|
++long_index;
|
|
}
|
|
|
|
if (p_sector->GetVisibleAtReplayStart())
|
|
{
|
|
Dbg_MsgAssert(long_index<numUint32s,("Too many sectors for bitfield !"));
|
|
p_bitfield[long_index] |= (1<<bit_index);
|
|
}
|
|
|
|
++bit_index;
|
|
if (bit_index>=32)
|
|
{
|
|
bit_index=0;
|
|
++long_index;
|
|
}
|
|
|
|
p_sector=p_table->IterateNext();
|
|
}
|
|
}
|
|
}
|
|
|
|
return long_index*32+bit_index;
|
|
#endif
|
|
}
|
|
|
|
void CEngine::sReadSectorStatusBitfield(uint32 *p_bitfield, int numUint32s)
|
|
{
|
|
return;
|
|
|
|
// Removed since we no longer have saving of replays to mem card.
|
|
// Leaving this code in was causing a problem in that the passed numUint32s
|
|
// was too small for the Skillzilla Park. Rather than increasing numUint32's
|
|
// and hence using up more memory, it was safer just to remove the contents of this function.
|
|
#if 0
|
|
Dbg_MsgAssert(p_bitfield,("NULL p_bitfield"));
|
|
|
|
int long_index=0;
|
|
int bit_index=0;
|
|
|
|
for (int i = 0; i<MAX_LOADED_SCENES; ++i)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
Lst::HashTable<CSector> *p_table=sp_loaded_scenes[i]->GetSectorList();
|
|
Dbg_MsgAssert(p_table,("NULL p_table"));
|
|
|
|
p_table->IterateStart();
|
|
CSector *p_sector=p_table->IterateNext();
|
|
while (p_sector)
|
|
{
|
|
Dbg_MsgAssert(long_index<numUint32s,("sReadSectorStatusBitfield: Too many sectors for bitfield !"));
|
|
if (p_bitfield[long_index] & (1<<bit_index))
|
|
{
|
|
p_sector->SetActiveAtReplayStart(true);
|
|
}
|
|
else
|
|
{
|
|
p_sector->SetActiveAtReplayStart(false);
|
|
}
|
|
|
|
++bit_index;
|
|
if (bit_index>=32)
|
|
{
|
|
bit_index=0;
|
|
++long_index;
|
|
}
|
|
|
|
Dbg_MsgAssert(long_index<numUint32s,("sReadSectorStatusBitfield: Too many sectors for bitfield !"));
|
|
if (p_bitfield[long_index] & (1<<bit_index))
|
|
{
|
|
p_sector->SetVisibleAtReplayStart(true);
|
|
}
|
|
else
|
|
{
|
|
p_sector->SetVisibleAtReplayStart(false);
|
|
}
|
|
|
|
++bit_index;
|
|
if (bit_index>=32)
|
|
{
|
|
bit_index=0;
|
|
++long_index;
|
|
}
|
|
|
|
p_sector=p_table->IterateNext();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void CEngine::sInitReplayStartState()
|
|
{
|
|
//printf("Calling sInitReplayStartState()\n");
|
|
for (int i = 0; i<MAX_LOADED_SCENES; ++i)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
Lst::HashTable<CSector> *p_table=sp_loaded_scenes[i]->GetSectorList();
|
|
Dbg_MsgAssert(p_table,("NULL p_table"));
|
|
|
|
p_table->IterateStart();
|
|
CSector *p_sector=p_table->IterateNext();
|
|
while (p_sector)
|
|
{
|
|
p_sector->SetActiveAtReplayStart(p_sector->IsActive());
|
|
p_sector->SetVisibleAtReplayStart(p_sector->GetVisibility()&0x01);
|
|
// Just to be safe
|
|
p_sector->SetStoredActive(p_sector->IsActive());
|
|
p_sector->SetStoredVisible(p_sector->GetVisibility()&0x01);
|
|
|
|
p_sector=p_table->IterateNext();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CEngine::sPrepareSectorsForReplayPlayback(bool store_initial_state)
|
|
{
|
|
//printf("Calling sPrepareSectorsForReplayPlayback()\n");
|
|
for (int i = 0; i<MAX_LOADED_SCENES; ++i)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
Lst::HashTable<CSector> *p_table=sp_loaded_scenes[i]->GetSectorList();
|
|
Dbg_MsgAssert(p_table,("NULL p_table"));
|
|
|
|
p_table->IterateStart();
|
|
CSector *p_sector=p_table->IterateNext();
|
|
while (p_sector)
|
|
{
|
|
if (store_initial_state)
|
|
{
|
|
p_sector->SetStoredActive(p_sector->IsActive());
|
|
p_sector->SetStoredVisible(p_sector->GetVisibility()&0x01);
|
|
}
|
|
|
|
p_sector->SetActive(p_sector->GetActiveAtReplayStart());
|
|
p_sector->SetVisibility(p_sector->GetVisibleAtReplayStart()?0xff:0x00);
|
|
|
|
p_sector=p_table->IterateNext();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CEngine::sRestoreSectorsAfterReplayPlayback()
|
|
{
|
|
//printf("Calling sRestoreSectorsAfterReplayPlayback()\n");
|
|
for (int i = 0; i<MAX_LOADED_SCENES; ++i)
|
|
{
|
|
if (sp_loaded_scenes[i])
|
|
{
|
|
Lst::HashTable<CSector> *p_table=sp_loaded_scenes[i]->GetSectorList();
|
|
Dbg_MsgAssert(p_table,("NULL p_table"));
|
|
|
|
p_table->IterateStart();
|
|
CSector *p_sector=p_table->IterateNext();
|
|
while (p_sector)
|
|
{
|
|
p_sector->SetActive(p_sector->GetStoredActive());
|
|
p_sector->SetVisibility(p_sector->GetStoredVisible()?0xff:0x00);
|
|
|
|
p_sector=p_table->IterateNext();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ScriptToggleAddScenes(Script::CStruct *pParams, Script::CScript *pScript)
|
|
{
|
|
// Just call engine function
|
|
CEngine::sToggleAddScenes();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ScriptSetScreenBlur(Script::CStruct *pParams, Script::CScript *pScript)
|
|
{
|
|
int amount = 0;
|
|
pParams->GetInteger(NONAME,&amount,false);
|
|
CEngine::sSetScreenBlur(amount);
|
|
return true;
|
|
}
|
|
|
|
|
|
// Given a script structure, add some metrics to it
|
|
// like number of objects, polygons, collision polygons, etc
|
|
void CEngine::sGetMetrics(Script::CStruct * p_info)
|
|
{
|
|
|
|
// Do main scene first, as it's more interesting
|
|
if (sGetMainScene())
|
|
{
|
|
Script::CStruct *p_main = new Script::CStruct();
|
|
sGetMainScene()->GetMetrics(p_main);
|
|
p_info->AddStructurePointer(CRCD(0x9b10525c,"MainScene"),p_main);
|
|
}
|
|
|
|
// But also do the sky, just in case
|
|
if (sGetSkyScene())
|
|
{
|
|
Script::CStruct *p_sky = new Script::CStruct();
|
|
sGetSkyScene()->GetMetrics(p_sky);
|
|
p_info->AddStructurePointer(CRCD(0x2c5ea34,"SkyScene"),p_sky);
|
|
}
|
|
|
|
#ifdef __PLAT_NGPS__
|
|
|
|
|
|
Script::CStruct *p_ps2 = new Script::CStruct();
|
|
p_ps2->AddInteger(CRCD(0x3dd01ea1,"total"), NxPs2::geom_stats_total);
|
|
p_ps2->AddInteger(CRCD(0x742cf11f,"inactive"), NxPs2::geom_stats_inactive );
|
|
p_ps2->AddInteger(CRCD(0xf9d98b10,"sky"), NxPs2::geom_stats_sky);
|
|
p_ps2->AddInteger(CRCD(0x6d65d103,"transformed"), NxPs2::geom_stats_transformed);
|
|
p_ps2->AddInteger(CRCD(0x52aa1a77,"skeletal"), NxPs2::geom_stats_skeletal);
|
|
p_ps2->AddInteger(CRCD(0xaf792e8d,"camera_sphere"), NxPs2::geom_stats_camera_sphere);
|
|
p_ps2->AddInteger(CRCD(0xa863e48c,"clipcull"), NxPs2::geom_stats_clipcull);
|
|
p_ps2->AddInteger(CRCD(0x93070c4b,"culled"), NxPs2::geom_stats_culled);
|
|
p_ps2->AddInteger(CRCD(0xac43724a,"leaf_culled"), NxPs2::geom_stats_leaf_culled);
|
|
p_ps2->AddInteger(CRCD(0x68306ffe,"boxcheck"), NxPs2::geom_stats_boxcheck);
|
|
p_ps2->AddInteger(CRCD(0xae81c5f6,"occludecheck"), NxPs2::geom_stats_occludecheck);
|
|
p_ps2->AddInteger(CRCD(0xba802ab0,"occluded"), NxPs2::geom_stats_occluded);
|
|
p_ps2->AddInteger(CRCD(0x1bc830f2,"colored"), NxPs2::geom_stats_colored);
|
|
p_ps2->AddInteger(CRCD(0x3960ff18,"leaf"), NxPs2::geom_stats_leaf);
|
|
p_ps2->AddInteger(CRCD(0x506a239f,"wibbleUV"), NxPs2::geom_stats_wibbleUV);
|
|
p_ps2->AddInteger(CRCD(0x169a94b7,"wibbleVC"), NxPs2::geom_stats_wibbleVC);
|
|
p_ps2->AddInteger(CRCD(0x7b471ba2,"sendcontext"), NxPs2::geom_stats_sendcontext);
|
|
p_ps2->AddInteger(CRCD(0x18725397,"sorted"), NxPs2::geom_stats_sorted);
|
|
p_ps2->AddInteger(CRCD(0x8a897dd2,"shadow"), NxPs2::geom_stats_shadow);
|
|
|
|
p_info->AddStructurePointer(CRCD(0xf2316b47,"PS2_Info"),p_ps2);
|
|
|
|
s_plat_get_metrics(p_info);
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
void CEngine::sSetLetterbox(bool letterbox)
|
|
{
|
|
s_plat_set_letterbox( letterbox );
|
|
}
|
|
|
|
|
|
|
|
/******************************************************************/
|
|
/* */
|
|
/* */
|
|
/******************************************************************/
|
|
void CEngine::sSetColorBufferClear(bool clear)
|
|
{
|
|
s_plat_set_color_buffer_clear( clear );
|
|
}
|
|
|
|
|
|
|
|
} // namespace Nx
|
|
|
|
|
|
|
|
|