commit about v 0.5.3

release comming(no DEMO)
This commit is contained in:
沈瑗杰 2021-10-02 20:40:06 +08:00
parent c882d7901c
commit 26506b6214
11 changed files with 114 additions and 430 deletions

View File

@ -15,26 +15,18 @@ gitee: @shenjackyuanjie
import os
import sys
import time
import random
import logging
import threading
import configparser
import multiprocessing
from decimal import Decimal
from multiprocessing import Pipe
from multiprocessing.connection import Connection
if __name__ == '__main__': # been start will not run this
sys.path.append('/bin/libs')
sys.path.append('/bin')
# Difficult_Rocket function
from Difficult_Rocket import crash
from Difficult_Rocket.api.Exp import *
from Difficult_Rocket.api.translate import tr
from Difficult_Rocket.graphics import widgets
from Difficult_Rocket.api import tools, load_file, new_thread, thread
from Difficult_Rocket.api import tools, new_thread, translate
# libs function
local_lib = True
@ -97,6 +89,7 @@ class ClientWindow(pyglet.window.Window):
pyglet.resource.path = ['textures']
pyglet.resource.reindex()
self.config_file = tools.load_file('configs/main.config')
self.game_config = tools.load_file('configs/game.config')
self.FPS = Decimal(int(self.config_file['runtime']['fps']))
self.SPF = Decimal('1') / self.FPS
# dic
@ -115,21 +108,32 @@ class ClientWindow(pyglet.window.Window):
self.M_frame = pyglet.gui.MovableFrame(self, modifier=key.LCTRL)
# setup
self.setup()
self.info_label = pyglet.text.Label(x=10, y=self.height - 10,
anchor_x='left', anchor_y='top',
batch=self.label_batch)
# 命令显示
self.command_label = [pyglet.text.Label(x=10, y=10 + 20 * line,
anchor_x='left', anchor_y='center',
font_name=translate.鸿蒙简体, font_size=12,
batch=self.label_batch)
for line in range(int(self.game_config['command']['show']) + 1)]
# fps显示
self.fps_label = pyglet.text.Label(x=10, y=self.height - 10,
anchor_x='left', anchor_y='top',
font_name=translate.鸿蒙简体, font_size=20,
batch=self.label_batch)
# 设置刷新率
pyglet.clock.schedule_interval(self.update, float(self.SPF))
# 完成设置后的信息输出
self.logger.info(tr.lang('window', 'setup.done'))
self.logger.info(tr.lang('window', 'os.pid_is').format(os.getpid(), os.getppid()))
end_time = time.time_ns()
self.use_time = end_time - start_time
self.logger.info(tr.lang('window', 'setup.use_time').format(Decimal(self.use_time) / 1000000000))
self.logger.debug(tr.lang('window', 'setup.use_time_ns').format(self.use_time))
def setup(self):
self.logger.info(tr.lang('window', 'os.pid_is').format(os.getpid(), os.getppid()))
self.load_fonts()
# print(pyglet.font.have_font('HarmonyOS_Sans_Black'))
@new_thread('client load_fonts')
# @new_thread('client load_fonts')
def load_fonts(self):
file_path = './libs/fonts/HarmonyOS Sans/'
ttf_files = os.listdir(file_path)
@ -176,7 +180,7 @@ class ClientWindow(pyglet.window.Window):
self.max_fps = [self.FPS, time.time()]
elif (time.time() - self.min_fps[1]) > self.fps_wait:
self.min_fps = [self.FPS, time.time()]
self.info_label.text = 'FPS: {:.1f} {:.1f} ({:.1f}/{:.1f}) | MSPF: {:.5f} '.format(now_FPS, 1 / tick, self.max_fps[0], self.min_fps[0], tick)
self.fps_label.text = 'FPS: {:.1f} {:.1f} ({:.1f}/{:.1f}) | MSPF: {:.5f} '.format(now_FPS, 1 / tick, self.max_fps[0], self.min_fps[0], tick)
def on_draw(self):
self.clear()
@ -184,7 +188,7 @@ class ClientWindow(pyglet.window.Window):
def on_resize(self, width: int, height: int):
super().on_resize(width, height)
self.info_label.y = height - 10
self.fps_label.y = height - 10
def draw_batch(self):
self.part_batch.draw()
@ -213,9 +217,16 @@ class ClientWindow(pyglet.window.Window):
key.MOD_CAPSLOCK |
key.MOD_SCROLLLOCK)):
self.dispatch_event('on_close')
self.logger.debug(tr.lang('window', 'key.press').format(key.symbol_string(symbol), key.modifiers_string(modifiers)))
def on_key_release(self, symbol, modifiers) -> None:
pass
self.logger.debug(tr.lang('window', 'key.release').format(key.symbol_string(symbol), key.modifiers_string(modifiers)))
def on_text(self, text):
if text == '':
self.logger.debug(tr.lang('window', 'text.new_line'))
else:
self.logger.debug(tr.lang('window', 'text.input').format(text))
def on_close(self) -> None:
self.run_input = False
@ -225,4 +236,4 @@ class ClientWindow(pyglet.window.Window):
config_file['window']['width'] = str(self.width)
config_file['window']['height'] = str(self.height)
config_file.write(open('configs/main.config', 'w', encoding='utf-8'))
super(ClientWindow, self).on_close()
super().on_close()

4
configs/game.config Normal file
View File

@ -0,0 +1,4 @@
[command]
log = 1000
show = 20
size = 12

View File

@ -6,6 +6,7 @@
'lang.language': 'English (EN-US)',
'logger.language': 'Logging language is: ',
'logger.created': 'Log handler created',
'logger.mkdir': 'logs/ folder not found, now created',
'logger.main_done': 'Main log handler created ',
'logger.logfile_name': 'Log file name :',
'logger.logfile_level': 'Log file record level :',
@ -14,17 +15,29 @@
'game_start.at': 'The main thread of the game starts with:'
},
'client': {
'setup.done': 'Client load complete ',
'os.pid_is': 'Client is using PID',
'setup.done': 'Client load complete',
'setup.use_time': 'Client load use: {} second',
'setup.use_time_ns': 'Client load use: {} nano second'
},
'window': {
'setup.done': 'Window load complete ',
'setup.use_time': 'Window load use: {} second',
'setup.use_time_ns': 'Window load use: {} nano second',
'os.pid_is': 'Window\'s PID: {} PPID: {}',
'button.been_press': 'The button is pressed, the current state of the button is:',
'mouse.press_at': 'mouse was click at {} button is: {}',
'mouse.release': 'mouse release at {} button is: {}',
'mouse.right': 'right button',
'mouse.left': 'left button'
'mouse.RIGHT': 'right button',
'mouse.LEFT': 'left button',
'mouse.MIDDLE': 'middle button',
'key.press': 'key {} {} been press',
'key.release': 'key {} {} been release',
'text.input': 'input text {}',
'text.new_line': '换行'
},
'server': {
'setup.done': 'server load complete ',
'os.pid_is': 'Client is using PID: {} PPID: {}'
'setup.done': 'Server load complete ',
'os.pid_is': 'Server is using PID: {} PPID: {}'
},
'language': 'English'
}

View File

@ -29,6 +29,11 @@
'mouse.release': '鼠标在 {} 释放,按键为:{}',
'mouse.RIGHT': '右键',
'mouse.LEFT': '左键',
'mouse.MIDDLE': '中键',
'key.press': '按键 {} {} 被按下',
'key.release': '按键 {} {} 被释放',
'text.input': '输入字符 {}',
'text.new_line': '换行',
'libs.local': '正在使用本地 pyglet 库 版本为: {}',
'libs.outer': '正在使用全局 pyglet 库 版本为: {}\n(可能会造成bug因为本地库版本为2.0dev9)',
'fonts.found': '在字体列表中找到以下字体库: {}'

View File

@ -35,15 +35,15 @@
},
'loggers': {
'client': {
'level': 'DEBUG',
// 'level': 'DEBUG',
'handlers': []
},
'server': {
'level': 'DEBUG',
// 'level': 'DEBUG',
'handlers': []
},
'main': {
'level': 'DEBUG',
// 'level': 'DEBUG',
'handlers': []
}
},

View File

@ -23,6 +23,9 @@
- `Difficult_Rocket.graphics.widgets.Parts`
- have many costume value
- `libs/fonts` now have `HarmonyOS_Sans`
- handler of `on_key_press` and `on_key_release` and `on_text`
- `game.config` config file
- `lang/en-us.json5` now up to date with `lang/zh-CN.json5`
## 20210928 V 0.5.2

View File

@ -514,31 +514,34 @@ class UniformBufferObject:
# Query the number of active Uniforms:
num_active = GLint()
indices = (GLuint * num_active.value)()
indices_ptr = cast(addressof(indices), POINTER(GLint))
glGetActiveUniformBlockiv(p_id, index, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, num_active)
glGetActiveUniformBlockiv(p_id, index, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices_ptr)
# Create objects and pointers for query values, to be used in the next step:
# Query the uniform index order and each uniform's offset:
indices = (GLuint * num_active.value)()
offsets = (GLint * num_active.value)()
gl_types = (GLuint * num_active.value)()
mat_stride = (GLuint * num_active.value)()
indices_ptr = cast(addressof(indices), POINTER(GLint))
offsets_ptr = cast(addressof(offsets), POINTER(GLint))
glGetActiveUniformBlockiv(p_id, index, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices_ptr)
glGetActiveUniformsiv(p_id, num_active.value, indices, GL_UNIFORM_OFFSET, offsets_ptr)
# Offsets may be returned in non-ascending order, sort them with the corresponding index:
_oi = sorted(zip(offsets, indices), key=lambda x: x[0])
offsets = [x[0] for x in _oi] + [self.block.size]
indices = (GLuint * num_active.value)(*(x[1] for x in _oi))
# Query other uniform information:
gl_types = (GLint * num_active.value)()
mat_stride = (GLint * num_active.value)()
gl_types_ptr = cast(addressof(gl_types), POINTER(GLint))
stride_ptr = cast(addressof(mat_stride), POINTER(GLint))
# Query the indices, offsets, and types uniforms:
glGetActiveUniformsiv(p_id, num_active.value, indices, GL_UNIFORM_OFFSET, offsets_ptr)
glGetActiveUniformsiv(p_id, num_active.value, indices, GL_UNIFORM_TYPE, gl_types_ptr)
glGetActiveUniformsiv(p_id, num_active.value, indices, GL_UNIFORM_MATRIX_STRIDE, stride_ptr)
offsets = offsets[:] + [self.block.size]
args = []
for i in range(num_active.value):
u_name, gl_type, length = self.block.uniforms[i]
start = offsets[i]
size = offsets[i+1] - start
u_name, gl_type, length = self.block.uniforms[indices[i]]
size = offsets[i+1] - offsets[i]
c_type_size = sizeof(gl_type)
actual_size = c_type_size * length
padding = size - actual_size

View File

@ -472,6 +472,17 @@ class Mat4(tuple):
0.0, 0.0, 1.0, 0.0,
vector[0], vector[1], vector[2], 1.0))
@classmethod
def from_rotation(cls, angle: float, vector: Vec3) -> 'Mat4':
"""Create a rotation matrix from an angle and Vec3.
:Parameters:
`angle` : A `float`
`vector` : A `Vec3`, or 3 component tuple of float or int
Vec3 or tuple with x, y and z translaton values
"""
return cls().rotate(angle, vector)
@classmethod
def look_at_direction(cls, direction: Vec3, up: Vec3) -> 'Mat4':
vec_z = direction.normalize()
@ -498,21 +509,22 @@ class Mat4(tuple):
"""Get a specific column as a tuple."""
return self[index::4]
def scale(self, x=1, y=1, z=1) -> 'Mat4':
def scale(self, vector: Vec3) -> 'Mat4':
"""Get a scale Matrix on x, y, or z axis."""
temp = list(self)
temp[0] *= x
temp[5] *= y
temp[10] *= z
temp[0] *= vector[0]
temp[5] *= vector[1]
temp[10] *= vector[2]
return Mat4(temp)
def translate(self, x=0, y=0, z=0) -> 'Mat4':
def translate(self, vector: Vec3) -> 'Mat4':
"""Get a translate Matrix along x, y, and z axis."""
return Mat4(self) @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1))
return Mat4(self) @ Mat4((1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, *vector, 1))
def rotate(self, angle=0, x=0, y=0, z=0) -> 'Mat4':
def rotate(self, angle: float, vector: Vec3) -> 'Mat4':
"""Get a rotation Matrix on x, y, or z axis."""
assert all(abs(n) <= 1 for n in (x, y, z)), "x,y,z must be normalized (<=1)"
assert all(abs(n) <= 1 for n in vector), "vector must be normalized (<=1)"
x, y, z = vector
c = _math.cos(angle)
s = _math.sin(angle)
t = 1 - c

View File

@ -89,7 +89,7 @@ import pyglet
from pyglet import gl
from pyglet import graphics
from pyglet.gl import current_context
from pyglet.math import Mat4
from pyglet.math import Mat4, Vec3
from pyglet.graphics import shader
from .codecs import ModelDecodeException
@ -190,8 +190,8 @@ class Model:
self.vertex_lists = vertex_lists
self.groups = groups
self._batch = batch
self._rotation = 0, 0, 0
self._translation = 0, 0, 0
self._rotation = Vec3()
self._translation = Vec3()
@property
def batch(self):
@ -279,16 +279,16 @@ class BaseMaterialGroup(graphics.ShaderGroup):
super().__init__(program, order, parent)
self.material = material
self.rotation = 0, 0, 0
self.translation = 0, 0, 0
self.rotation = Vec3()
self.translation = Vec3()
def set_modelview_matrix(self):
# NOTE: Matrix operations can be optimized later with transform feedback
view = Mat4()
view = view.rotate(radians(self.rotation[2]), z=1)
view = view.rotate(radians(self.rotation[1]), y=1)
view = view.rotate(radians(self.rotation[0]), x=1)
view = view.translate(*self.translation)
view = view.rotate(radians(self.rotation[2]), Vec3(0, 0, 1))
view = view.rotate(radians(self.rotation[1]), Vec3(0, 1, 0))
view = view.rotate(radians(self.rotation[0]), Vec3(1, 0, 0))
view = view.translate(self.translation)
# TODO: separate the projection block, and remove this hack
block = self.program.uniform_blocks['WindowBlock']

View File

@ -178,141 +178,6 @@ fragment_array_source = """#version 150 core
"""
############################
vertex_src = """#version 150
in vec2 position;
in vec4 size;
in vec4 color;
in vec4 tex_coords;
in float rotation;
out vec4 geo_size;
out vec4 geo_color;
out vec4 geo_tex_coords;
out float geo_rotation;
void main() {
gl_Position = vec4(position, 0, 1);
geo_size = size;
geo_color = color;
geo_tex_coords = tex_coords;
geo_rotation = rotation;
}
"""
geometry_src = """#version 150
// We are taking single points form the vertex shader
// and emitting 4 new vertices creating a quad/sprites
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
uniform WindowBlock
{
mat4 projection;
mat4 view;
} window;
// Since geometry shader can take multiple values from a vertex
// shader we need to define the inputs from it as arrays.
// In our instance we just take single values (points)
in vec4 geo_size[];
in vec4 geo_color[];
in vec4 geo_tex_coords[];
in float geo_rotation[];
out vec2 uv;
out vec4 frag_color;
void main() {
// We grab the position value from the vertex shader
vec2 center = gl_in[0].gl_Position.xy;
// Calculate the half size of the sprites for easier calculations
vec2 hsize = geo_size[0].xy / 2.0;
// Convert the rotation to radians
float angle = radians(-geo_rotation[0]);
// Create a scale vector
vec2 scale = vec2(geo_size[0][2], geo_size[0][3]);
// Create a 2d rotation matrix
mat2 rot = mat2(cos(angle), sin(angle),
-sin(angle), cos(angle));
// Calculate the left, bottom, right, top:
float tl = geo_tex_coords[0].s;
float tb = geo_tex_coords[0].t;
float tr = geo_tex_coords[0].s + geo_tex_coords[0].p;
float tt = geo_tex_coords[0].t + geo_tex_coords[0].q;
// Emit a triangle strip creating a quad (4 vertices).
// Here we need to make sure the rotation is applied before we position the sprite.
// We just use hardcoded texture coordinates here. If an atlas is used we
// can pass an additional vec4 for specific texture coordinates.
// Each EmitVertex() emits values down the shader pipeline just like a single
// run of a vertex shader, but in geomtry shaders we can do it multiple times!
// Upper left
gl_Position = window.projection * window.view * vec4(rot * vec2(-hsize.x, hsize.y) * scale + center, 0.0, 1.0);
uv = vec2(tl, tt);
frag_color = geo_color[0];
EmitVertex();
// lower left
gl_Position = window.projection * window.view * vec4(rot * vec2(-hsize.x, -hsize.y) * scale + center, 0.0, 1.0);
uv = vec2(tl, tb);
frag_color = geo_color[0];
EmitVertex();
// upper right
gl_Position = window.projection * window.view * vec4(rot * vec2(hsize.x, hsize.y) * scale + center, 0.0, 1.0);
uv = vec2(tr, tt);
frag_color = geo_color[0];
EmitVertex();
// lower right
gl_Position = window.projection * window.view * vec4(rot * vec2(hsize.x, -hsize.y) * scale + center, 0.0, 1.0);
uv = vec2(tr, tb);
frag_color = geo_color[0];
EmitVertex();
// We are done with this triangle strip now
EndPrimitive();
}
"""
fragment_src = """#version 150
in vec2 uv;
in vec4 frag_color;
out vec4 final_color;
uniform sampler2D sprite_texture;
void main() {
final_color = texture(sprite_texture, uv) * frag_color;
}
"""
def get_geo_shader():
try:
return pyglet.gl.current_context.pyglet_sprite_geo_shader
except AttributeError:
new_vert_shader = graphics.shader.Shader(vertex_src, 'vertex')
new_geom_shader = graphics.shader.Shader(geometry_src, 'geometry')
new_frag_shader = graphics.shader.Shader(fragment_src, 'fragment')
new_program = graphics.shader.ShaderProgram(new_vert_shader, new_geom_shader, new_frag_shader)
pyglet.gl.current_context.pyglet_sprite_geo_shader = new_program
return pyglet.gl.current_context.pyglet_sprite_geo_shader
def get_default_shader():
try:
return pyglet.gl.current_context.pyglet_sprite_default_shader
@ -935,229 +800,3 @@ class Sprite(event.EventDispatcher):
Sprite.register_event_type('on_animation_end')
class GeoSprite(Sprite):
"""Instance of an on-screen image.
See the module documentation for usage.
"""
_batch = None
_animation = None
_frame_index = 0
_paused = False
_rotation = 0
_rgba = [255, 255, 255, 255]
_scale = 1.0
_scale_x = 1.0
_scale_y = 1.0
_visible = True
def __init__(self,
img, x=0, y=0,
blend_src=GL_SRC_ALPHA,
blend_dest=GL_ONE_MINUS_SRC_ALPHA,
batch=None,
group=None,
subpixel=False):
"""Create a sprite.
:Parameters:
`img` : `~pyglet.image.AbstractImage` or `~pyglet.image.Animation`
Image or animation to display.
`x` : int
X coordinate of the sprite.
`y` : int
Y coordinate of the sprite.
`blend_src` : int
OpenGL blend source mode. The default is suitable for
compositing sprites drawn from back-to-front.
`blend_dest` : int
OpenGL blend destination mode. The default is suitable for
compositing sprites drawn from back-to-front.
`batch` : `~pyglet.graphics.Batch`
Optional batch to add the sprite to.
`group` : `~pyglet.graphics.Group`
Optional parent group of the sprite.
`subpixel` : bool
Allow floating-point coordinates for the sprite. By default,
coordinates are restricted to integer values.
"""
self._x = x
self._y = y
if isinstance(img, image.Animation):
self._animation = img
self._texture = img.frames[0].image.get_texture()
self._next_dt = img.frames[0].duration
if self._next_dt:
clock.schedule_once(self._animate, self._next_dt)
else:
self._texture = img.get_texture()
program = get_geo_shader()
self._batch = batch or graphics.get_default_batch()
self._group = SpriteGroup(self._texture, blend_src, blend_dest, program, 0, group)
self._subpixel = subpixel
# TODO: add a property to the Texture class:
tex_coords = (self._texture.tex_coords[0], self._texture.tex_coords[1],
self._texture.tex_coords[3], self._texture.tex_coords[7])
self._vertex_list = self._batch.add(
1, GL_POINTS, self._group,
('position2f', (int(x) if subpixel else x, int(y) if subpixel else y)),
('size4f', (self._texture.width, self._texture.height, 1, 1)),
('rotation1f', (self._rotation,)),
('color4Bn', self._rgba),
('tex_coords4f', tex_coords)
)
@property
def position(self):
"""The (x, y) coordinates of the sprite, as a tuple.
:Parameters:
`x` : int
X coordinate of the sprite.
`y` : int
Y coordinate of the sprite.
"""
return self._x, self._y
@position.setter
def position(self, position):
self._x, self._y = position
self._vertex_list.position[:] = position
@property
def x(self):
"""X coordinate of the sprite.
:type: int
"""
return self._x
@x.setter
def x(self, x):
self._x = x
self._vertex_list.position[:] = x, self._y
@property
def y(self):
"""Y coordinate of the sprite.
:type: int
"""
return self._y
@y.setter
def y(self, y):
self._y = y
self._vertex_list.position[:] = self._x, y
@property
def rotation(self):
"""Clockwise rotation of the sprite, in degrees.
The sprite image will be rotated about its image's (anchor_x, anchor_y)
position.
:type: float
"""
return self._rotation
@rotation.setter
def rotation(self, rotation):
self._rotation = rotation
self._vertex_list.rotation[0] = rotation
@property
def scale(self):
"""Base Scaling factor.
A scaling factor of 1 (the default) has no effect. A scale of 2 will
draw the sprite at twice the native size of its image.
:type: float
"""
return self._scale
@scale.setter
def scale(self, scale):
self._scale = scale
self._vertex_list.size[2:4] = (scale * self._scale_x, scale * self._scale_y)
@property
def scale_x(self):
"""Horizontal scaling factor.
A scaling factor of 1 (the default) has no effect. A scale of 2 will
draw the sprite at twice the native width of its image.
:type: float
"""
return self._scale_x
@scale_x.setter
def scale_x(self, scale_x):
self._scale_x = scale_x
self._vertex_list.size[2:4] = (self._scale * scale_x, self._scale * self._scale_y)
@property
def scale_y(self):
"""Vertical scaling factor.
A scaling factor of 1 (the default) has no effect. A scale of 2 will
draw the sprite at twice the native height of its image.
:type: float
"""
return self._scale_y
@scale_y.setter
def scale_y(self, scale_y):
self._scale_y = scale_y
self._vertex_list.size[2:4] = (self._scale * self._scale_x, self._scale * scale_y)
@property
def opacity(self):
"""Blend opacity.
This property sets the alpha component of the colour of the sprite's
vertices. With the default blend mode (see the constructor), this
allows the sprite to be drawn with fractional opacity, blending with the
background.
An opacity of 255 (the default) has no effect. An opacity of 128 will
make the sprite appear translucent.
:type: int
"""
return self._rgba[3]
@opacity.setter
def opacity(self, opacity):
self._rgba[3] = opacity
self._vertex_list.color[:] = self._rgba
@property
def color(self):
"""Blend color.
This property sets the color of the sprite's vertices. This allows the
sprite to be drawn with a color tint.
The color is specified as an RGB tuple of integers '(red, green, blue)'.
Each color component must be in the range 0 (dark) to 255 (saturated).
:type: (int, int, int)
"""
return self._rgba[:3]
@color.setter
def color(self, rgb):
self._rgba[:3] = list(map(int, rgb))
self._vertex_list.color[:] = self._rgba

View File

@ -1011,18 +1011,8 @@ class TextLayout:
return self._x, self._y
@position.setter
def position(self, position):
self.update(*position)
def update(self, x, y):
"""Change both X and Y positions of the layout at once for faster performance.
:Parameters:
`x` : int
X coordinate of the layout.
`y` : int
Y coordinate of the layout.
"""
def position(self, values):
x, y = values
if self._boxes:
self._x = x
self._y = y
@ -1790,6 +1780,10 @@ class ScrollableTextLayout(TextLayout):
for group in self.groups.values():
group.scissor_area = area
def _update(self):
super()._update()
self._update_scissor_area()
# Properties
@property