711 lines
17 KiB
C++
711 lines
17 KiB
C++
/*
|
|
** Command & Conquer Renegade(tm)
|
|
** Copyright 2025 Electronic Arts Inc.
|
|
**
|
|
** This program is free software: you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation, either version 3 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/***********************************************************************************************
|
|
*** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S ***
|
|
***********************************************************************************************
|
|
* *
|
|
* Project Name : Combat *
|
|
* *
|
|
* $Archive:: /Commando/Code/wwui/viewerctrl.cpp $*
|
|
* *
|
|
* Author:: Patrick Smith *
|
|
* *
|
|
* $Modtime:: 11/06/01 4:23p $*
|
|
* *
|
|
* $Revision:: 9 $*
|
|
* *
|
|
*---------------------------------------------------------------------------------------------*
|
|
* Functions: *
|
|
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
|
|
|
// Disable warning about exception handling not being enabled. It's used as part of STL - in a part of STL we don't use.
|
|
#pragma warning(disable : 4530)
|
|
|
|
#include "viewerctrl.h"
|
|
#include "assetmgr.h"
|
|
#include "refcount.h"
|
|
#include "mousemgr.h"
|
|
#include "ww3d.h"
|
|
#include "dialogmgr.h"
|
|
#include "dialogbase.h"
|
|
#include "stylemgr.h"
|
|
#include "scene.h"
|
|
#include "camera.h"
|
|
#include "rendobj.h"
|
|
#include "hanim.h"
|
|
#include "mesh.h"
|
|
#include "meshmdl.h"
|
|
#include "light.h"
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
// Local constants
|
|
////////////////////////////////////////////////////////////////
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// ViewerCtrlClass
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
ViewerCtrlClass::ViewerCtrlClass (void) :
|
|
Scene (NULL),
|
|
Camera (NULL),
|
|
Model (NULL),
|
|
Distance (0),
|
|
ZRotation (0),
|
|
MinCameraDist (0),
|
|
RotationRate (DEG_TO_RADF (0.0f)),
|
|
BoundingBox (Vector3 (0, 0, 0), Vector3 (0, 0, 0)),
|
|
IsCameraDirty (true),
|
|
IsBackgroundVisible (true),
|
|
InterfaceMode (Z_ROTATION)
|
|
{
|
|
Vector3 ambientcolor (0.2f, 0.2f, 0.2f);
|
|
|
|
LastMousePosition = DialogMgrClass::Get_Mouse_Pos();
|
|
|
|
//
|
|
// Configure the 2D renderer
|
|
//
|
|
StyleMgrClass::Configure_Renderer (&ControlRenderer);
|
|
|
|
//
|
|
// Create a scene to use to render the model
|
|
//
|
|
Scene = new SimpleSceneClass;
|
|
Scene->Set_Ambient_Light (ambientcolor);
|
|
|
|
//
|
|
// Create a camera to use in the scene
|
|
// NOTE: Near and far clip planes are set to values which will suit a large range of object sizes.
|
|
// However, very small objects may pass thru the near clip plane and large objects may pass
|
|
// thru the far clip plane (and Z-fighting will result if the ratio is too high). If this
|
|
// proves to problematic replace the constant settings with a function of object size that
|
|
// can be set on a per object basis.
|
|
//
|
|
Camera = new CameraClass;
|
|
Camera->Set_Clip_Planes (0.25F, 300.0F);
|
|
|
|
// Create and add a light.
|
|
Light = NEW_REF (LightClass, ());
|
|
Light->Set_Intensity (1.0f);
|
|
Light->Set_Ambient (Vector3 (0.0f, 0.0f, 0.0f));
|
|
Light->Set_Diffuse (Vector3 (1.0f, 1.0f, 1.0f));
|
|
Scene->Add_Render_Object (Light);
|
|
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// ~ViewerCtrlClass
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
ViewerCtrlClass::~ViewerCtrlClass (void)
|
|
{
|
|
Free_Model ();
|
|
REF_PTR_RELEASE (Light);
|
|
REF_PTR_RELEASE (Camera);
|
|
REF_PTR_RELEASE (Scene);
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Create_Control_Renderer
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Create_Control_Renderer (void)
|
|
{
|
|
Render2DClass &renderer = ControlRenderer;
|
|
|
|
//
|
|
// Configure this renderer
|
|
//
|
|
renderer.Reset ();
|
|
renderer.Enable_Texturing (false);
|
|
|
|
ShaderClass *shader = renderer.Get_Shader ();
|
|
renderer.Set_Z_Value (1.0F);
|
|
shader->Set_Depth_Compare (ShaderClass::PASS_ALWAYS);
|
|
shader->Set_Depth_Mask (ShaderClass::DEPTH_WRITE_ENABLE);
|
|
shader->Set_Color_Mask (ShaderClass::COLOR_WRITE_ENABLE);
|
|
|
|
|
|
//
|
|
// Determine which color to draw the outline in
|
|
//
|
|
int color = StyleMgrClass::Get_Line_Color ();
|
|
int bkcolor = StyleMgrClass::Get_Bk_Color ();
|
|
if (IsEnabled == false) {
|
|
color = StyleMgrClass::Get_Disabled_Line_Color ();
|
|
bkcolor = StyleMgrClass::Get_Disabled_Bk_Color ();
|
|
}
|
|
|
|
//
|
|
// Draw the window frame
|
|
//
|
|
renderer.Add_Rect (Rect, 1.0F, color, bkcolor);
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// On_Set_Cursor
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::On_Set_Cursor (const Vector2 &mouse_pos)
|
|
{
|
|
//
|
|
// Change the mouse cursor
|
|
//
|
|
MouseMgrClass::Set_Cursor (MouseMgrClass::CURSOR_ROTATE);
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Update_Client_Rect
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Update_Client_Rect (void)
|
|
{
|
|
//
|
|
// Set the client area
|
|
//
|
|
ClientRect = Rect;
|
|
|
|
//
|
|
// Calculate what the horizontal and vertical field of view
|
|
// should be for this window.
|
|
//
|
|
float hfov = 0;
|
|
float vfov = 0;
|
|
float cx = Rect.Width ();
|
|
float cy = Rect.Height ();
|
|
if (cy > cx) {
|
|
vfov = DEG_TO_RADF (45.0F);
|
|
hfov = (cx / cy) * vfov;
|
|
} else {
|
|
hfov = DEG_TO_RADF (45.0F);
|
|
vfov = (cy / cx) * hfov;
|
|
}
|
|
|
|
//
|
|
// Set the new view plane for the camera
|
|
//
|
|
Camera->Set_View_Plane (hfov, vfov);
|
|
|
|
//
|
|
// Calculate a viewport that we can use to restraint
|
|
// the model to our window.
|
|
//
|
|
const RectClass &screen_rect = Render2DClass::Get_Screen_Resolution ();
|
|
Vector2 upper_left = Rect.Upper_Left ();
|
|
Vector2 lower_right = Rect.Lower_Right ();
|
|
|
|
upper_left.X /= screen_rect.Width ();
|
|
lower_right.X /= screen_rect.Width ();
|
|
upper_left.Y /= screen_rect.Height ();
|
|
lower_right.Y /= screen_rect.Height ();
|
|
|
|
//
|
|
// Set the viewport to render only to our window
|
|
//
|
|
Camera->Set_Viewport (upper_left, lower_right);
|
|
|
|
Set_Dirty ();
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Render
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Render (void)
|
|
{
|
|
//
|
|
// Recreate the renderers (if necessary)
|
|
//
|
|
if (IsDirty) {
|
|
Create_Control_Renderer ();
|
|
}
|
|
|
|
//
|
|
// Render the background
|
|
//
|
|
if (IsBackgroundVisible) {
|
|
ControlRenderer.Render ();
|
|
}
|
|
|
|
//
|
|
// Render the 3D object
|
|
//
|
|
if (Scene != NULL) {
|
|
WW3D::Render (Scene, Camera);
|
|
}
|
|
|
|
DialogControlClass::Render ();
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// On_LButton_Down
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::On_LButton_Down (const Vector2 &mouse_pos)
|
|
{
|
|
// Put the viewer interface into virtual trackball mode.
|
|
Set_Interface_Mode (ViewerCtrlClass::VIRTUAL_TRACKBALL);
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// On_LButton_Up
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::On_LButton_Up (const Vector2 &mouse_pos)
|
|
{
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Set_Model
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Set_Model (const char *model_name)
|
|
{
|
|
//
|
|
// Load the new model
|
|
//
|
|
RenderObjClass *new_model = WW3DAssetManager::Get_Instance ()->Create_Render_Obj (model_name);
|
|
Set_Model (new_model);
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Set_Model
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Set_Model (RenderObjClass *new_model)
|
|
{
|
|
//
|
|
// Start fresh
|
|
//
|
|
Free_Model ();
|
|
|
|
if (new_model != NULL) {
|
|
Model = new_model;
|
|
|
|
//
|
|
// Notify any advise sinks
|
|
//
|
|
ADVISE_NOTIFY (On_ViewerCtrl_Model_Loaded (this, Get_ID (), Model));
|
|
|
|
//
|
|
// Force the high LOD
|
|
//
|
|
int count = Model->Get_LOD_Count ();
|
|
Model->Set_LOD_Level (max (count-1, 0));
|
|
Model->Set_Transform (Matrix3D (1));
|
|
|
|
//
|
|
// Add the model to the scene
|
|
//
|
|
Scene->Add_Render_Object (Model);
|
|
|
|
//
|
|
// Force a camera update
|
|
//
|
|
IsCameraDirty = true;
|
|
}
|
|
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Calculate_Camera_Position
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Calculate_Camera_Position (void)
|
|
{
|
|
if (Model == NULL) {
|
|
return ;
|
|
}
|
|
|
|
Model->Update_Obj_Space_Bounding_Volumes ();
|
|
Model->Update_Sub_Object_Transforms ();
|
|
|
|
//
|
|
// Get the bounds of this object
|
|
//
|
|
bool is_first = true;
|
|
BoundingBox.Center.Set (0, 0, 0);
|
|
BoundingBox.Extent.Set (0, 0, 0);
|
|
Get_Visible_Bounding_Box (&BoundingBox, Model, is_first);
|
|
|
|
//
|
|
// Get information about the camera
|
|
//
|
|
Vector2 vpmin, vpmax;
|
|
Camera->Get_View_Plane (vpmin, vpmax);
|
|
|
|
float znear = 0;
|
|
float zfar = 0;
|
|
Camera->Get_Clip_Planes (znear, zfar);
|
|
|
|
//
|
|
// Calculate the closest distance we can get
|
|
// without clipping the rotating object
|
|
//
|
|
float width = WWMath::Sqrt (BoundingBox.Extent.X * BoundingBox.Extent.X * 4 + BoundingBox.Extent.Y * BoundingBox.Extent.Y * 4);
|
|
float height = BoundingBox.Extent.Z * 2;
|
|
|
|
float near_width = (vpmax.X - vpmin.X) * znear;
|
|
float far_width = (vpmax.X - vpmin.X) * zfar;
|
|
|
|
float near_height = (vpmax.Y - vpmin.Y) * znear;
|
|
float far_height = (vpmax.Y - vpmin.Y) * zfar;
|
|
|
|
float percent1 = (width - near_width) / (far_width - near_width);
|
|
float percent2 = (height - near_height) / (far_height - near_height);
|
|
|
|
float test_z1 = znear + (zfar - znear) * percent1;
|
|
float test_z2 = znear + (zfar - znear) * percent2;
|
|
|
|
Distance = (max (test_z1, test_z2) * 1.2F);
|
|
Distance = max (MinCameraDist, Distance);
|
|
|
|
//
|
|
// Reposition the camera to be looking at this position
|
|
//
|
|
Vector3 center = BoundingBox.Center;
|
|
Vector3 pos = center + Vector3 (Distance, 0, 0);
|
|
Matrix3D tm;
|
|
tm.Look_At (pos, center, 0);
|
|
Camera->Set_Transform (tm);
|
|
|
|
//
|
|
// Remember that's we've updated
|
|
//
|
|
IsCameraDirty = false;
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Set_Animation
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Set_Animation (const char *anim_name)
|
|
{
|
|
if (Model == NULL) {
|
|
return ;
|
|
}
|
|
|
|
if (anim_name[0] != 0) {
|
|
|
|
//
|
|
// Play the animation on the model
|
|
//
|
|
HAnimClass *anim = WW3DAssetManager::Get_Instance ()->Get_HAnim (anim_name);
|
|
if (anim != NULL) {
|
|
Model->Set_Animation (anim, 0, RenderObjClass::ANIM_MODE_LOOP);
|
|
REF_PTR_RELEASE (anim);
|
|
}
|
|
|
|
} else {
|
|
|
|
//
|
|
// Stop the animation
|
|
//
|
|
Model->Set_Animation ();
|
|
}
|
|
|
|
//
|
|
// Force a camera update
|
|
//
|
|
IsCameraDirty = true;
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Free_Model
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Free_Model (void)
|
|
{
|
|
if (Model != NULL) {
|
|
Model->Remove ();
|
|
REF_PTR_RELEASE (Model);
|
|
}
|
|
|
|
return ;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// Set_Interface_Mode
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void ViewerCtrlClass::Set_Interface_Mode (InterfaceModeEnum mode, float rotationrate)
|
|
{
|
|
// If already in the desired mode do not reset the rotation.
|
|
if (mode != InterfaceMode) {
|
|
InterfaceMode = mode;
|
|
ZRotation = DEG_TO_RADF (0.0f);
|
|
}
|
|
RotationRate = DEG_TO_RADF (rotationrate);
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////
|
|
//
|
|
// On_Frame_Update
|
|
//
|
|
////////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::On_Frame_Update (void)
|
|
{
|
|
//
|
|
// Update the camera's data if necessary
|
|
//
|
|
if (IsCameraDirty) {
|
|
Calculate_Camera_Position ();
|
|
}
|
|
|
|
switch (InterfaceMode) {
|
|
|
|
case Z_ROTATION:
|
|
{
|
|
float delta = ((DialogMgrClass::Get_Frame_Time () / 1000.0F) * RotationRate);
|
|
|
|
//
|
|
// Adjust our rotation
|
|
//
|
|
ZRotation -= delta;
|
|
if (ZRotation < 0) {
|
|
ZRotation += DEG_TO_RADF (360);
|
|
}
|
|
|
|
//
|
|
// Rotate the camera's position about the object
|
|
//
|
|
Matrix3D rotation_tm (1);
|
|
rotation_tm.Rotate_Z (ZRotation);
|
|
Vector3 position = rotation_tm.Rotate_Vector (Vector3 (Distance, 0, 0)) + BoundingBox.Center;
|
|
|
|
//
|
|
// Update the camera's position
|
|
//
|
|
Matrix3D tm;
|
|
tm.Look_At (position, BoundingBox.Center, 0);
|
|
Camera->Set_Transform (tm);
|
|
break;
|
|
}
|
|
|
|
case VIRTUAL_TRACKBALL:
|
|
|
|
if (DialogMgrClass::Is_Button_Down (VK_LBUTTON)) {
|
|
|
|
const RectClass &rect = Get_Window_Rect();
|
|
const Vector2 mouseposition (DialogMgrClass::Get_Mouse_Pos().X, DialogMgrClass::Get_Mouse_Pos().Y);
|
|
|
|
if (rect.Contains (mouseposition)) {
|
|
|
|
int width, height, bits;
|
|
bool windowed;
|
|
float oow, ooh;
|
|
Vector2 viewportmin, viewportmax, viewportextent, viewportcenter;
|
|
float a, b;
|
|
Vector2 p0, p1;
|
|
Quaternion rotation;
|
|
Matrix3D transform, inversetransform;
|
|
Vector3 objectcenter;
|
|
|
|
WW3D::Get_Render_Target_Resolution (width, height, bits, windowed);
|
|
oow = 1.0f / width;
|
|
ooh = 1.0f / height;
|
|
|
|
Camera->Get_Viewport (viewportmin, viewportmax);
|
|
viewportextent = viewportmax - viewportmin;
|
|
viewportcenter = 0.5f * (viewportmin + viewportmax);
|
|
a = 1.0f / viewportextent.X;
|
|
b = -1.0f / viewportextent.Y;
|
|
|
|
p0.Set (LastMousePosition.X, LastMousePosition.Y);
|
|
p0.Scale (oow, ooh);
|
|
p0 -= viewportcenter;
|
|
p0.Scale (a, b);
|
|
|
|
p1.Set (DialogMgrClass::Get_Mouse_Pos().X, DialogMgrClass::Get_Mouse_Pos().Y);
|
|
p1.Scale (oow, ooh);
|
|
p1 -= viewportcenter;
|
|
p1.Scale (a, b);
|
|
|
|
rotation = ::Trackball (p0.X, p0.Y, p1.X, p1.Y, 0.5f);
|
|
|
|
transform = Camera->Get_Transform();
|
|
transform.Get_Orthogonal_Inverse (inversetransform);
|
|
objectcenter = inversetransform * BoundingBox.Center;
|
|
transform.Translate (objectcenter);
|
|
Matrix3D::Multiply (transform, Build_Matrix3D (rotation), &transform);
|
|
transform.Translate (-objectcenter);
|
|
|
|
Camera->Set_Transform (transform);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Place the light source at the camera position.
|
|
Light->Set_Transform (Camera->Get_Transform());
|
|
|
|
LastMousePosition = DialogMgrClass::Get_Mouse_Pos();
|
|
DialogControlClass::On_Frame_Update ();
|
|
return ;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
//
|
|
// Get_Visible_Bounding_Box
|
|
//
|
|
///////////////////////////////////////////////////////////////
|
|
void
|
|
ViewerCtrlClass::Get_Visible_Bounding_Box (AABoxClass *box, RenderObjClass *render_obj, bool &is_first)
|
|
{
|
|
if (render_obj == NULL) {
|
|
return ;
|
|
}
|
|
|
|
//
|
|
// Recursively walk through the subobjects
|
|
//
|
|
for (int index = 0; index < render_obj->Get_Num_Sub_Objects (); index ++) {
|
|
RenderObjClass *sub_obj = render_obj->Get_Sub_Object (index);
|
|
|
|
//
|
|
// Recurse into this sub-object
|
|
//
|
|
if (sub_obj != NULL && (sub_obj->Is_Hidden () == false)) {
|
|
Get_Visible_Bounding_Box (box, sub_obj, is_first);
|
|
}
|
|
REF_PTR_RELEASE (sub_obj);
|
|
}
|
|
|
|
//
|
|
// Is this a mesh?
|
|
//
|
|
if (render_obj->Class_ID () == RenderObjClass::CLASSID_MESH) {
|
|
MeshClass *mesh = reinterpret_cast<MeshClass *> (render_obj);
|
|
|
|
//
|
|
// Dig out the mesh's model
|
|
//
|
|
MeshModelClass *mesh_model = mesh->Get_Model ();
|
|
if (mesh_model != NULL) {
|
|
|
|
//
|
|
// Get the vertex count
|
|
//
|
|
int vertex_count = mesh_model->Get_Vertex_Count ();
|
|
Vector3 *vertex_array = NULL;
|
|
|
|
//
|
|
// Get a pointer to the vertices
|
|
//
|
|
bool is_skin = (mesh_model->Get_Flag (MeshGeometryClass::SKIN) != 0);
|
|
if (is_skin) {
|
|
vertex_array = new Vector3[vertex_count];
|
|
mesh->Get_Deformed_Vertices (vertex_array);
|
|
} else {
|
|
vertex_array = mesh_model->Get_Vertex_Array ();
|
|
}
|
|
|
|
//
|
|
// Loop over all the verts inside this model
|
|
//
|
|
for (int index = 0; index < vertex_count; index ++) {
|
|
|
|
//
|
|
// Get the world-space vertex position
|
|
//
|
|
Vector3 vertex_pos = vertex_array[index];
|
|
if (is_skin == false) {
|
|
vertex_pos = mesh->Get_Transform () * vertex_array[index];
|
|
}
|
|
|
|
//
|
|
// Expand our box to account for this vertex
|
|
//
|
|
if (is_first) {
|
|
box->Center = vertex_pos;
|
|
is_first = false;
|
|
} else {
|
|
box->Add_Point (vertex_pos);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Free the vertex array (if necessary)
|
|
//
|
|
if (is_skin) {
|
|
delete [] vertex_array;
|
|
vertex_array = NULL;
|
|
}
|
|
|
|
REF_PTR_RELEASE (mesh_model);
|
|
}
|
|
}
|
|
|
|
return ;
|
|
}
|