feat(core): 启用 TypeScript 最严格的类型检查 (#199)
* feat(core): 启用 TypeScript 最严格的类型检查 * ci: 配置 Codecov 以适应类型安全改进 * fix(core): 修复 CodeQL 安全警告 * fix(core): eslint.config.mjs
This commit is contained in:
@@ -25,7 +25,7 @@ export interface BitMask64Data {
|
||||
|
||||
export class BitMask64Utils {
|
||||
/** 零掩码常量,所有位都为0 */
|
||||
public static readonly ZERO: Readonly<BitMask64Data> = { base: [0, 0], segments: undefined };
|
||||
public static readonly ZERO: Readonly<BitMask64Data> = { base: [0, 0] };
|
||||
|
||||
/**
|
||||
* 根据位索引创建64位掩码
|
||||
@@ -37,7 +37,7 @@ export class BitMask64Utils {
|
||||
if (bitIndex < 0) {
|
||||
throw new Error(`Bit index ${bitIndex} out of range [0, ∞)`);
|
||||
}
|
||||
const mask: BitMask64Data = { base: [0, 0], segments: undefined };
|
||||
const mask: BitMask64Data = { base: [0, 0] };
|
||||
BitMask64Utils.setBit(mask, bitIndex);
|
||||
return mask;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export class BitMask64Utils {
|
||||
* @returns 低32位为输入值、高32位为0的掩码
|
||||
*/
|
||||
public static fromNumber(value: number): BitMask64Data {
|
||||
return { base: [value >>> 0, 0], segments: undefined};
|
||||
return { base: [value >>> 0, 0] };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +66,10 @@ export class BitMask64Utils {
|
||||
// 基础区段就包含指定的位,或任意一个参数不含扩展区段,直接短路
|
||||
if(baseHasAny || !bitsSegments || !maskSegments) return baseHasAny;
|
||||
// 额外检查扩展区域是否包含指定的位 - 如果bitsSegments[index]不存在,会被转为NaN,NaN的位运算始终返回0
|
||||
return maskSegments.some((seg, index) => (seg[SegmentPart.LOW] & bitsSegments[index][SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSegments[index][SegmentPart.HIGH]) !== 0)
|
||||
return maskSegments.some((seg, index) => {
|
||||
const bitsSeg = bitsSegments[index];
|
||||
return bitsSeg && ((seg[SegmentPart.LOW] & bitsSeg[SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSeg[SegmentPart.HIGH]) !== 0);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +92,9 @@ export class BitMask64Utils {
|
||||
// 对mask/bits中都存在的区段,进行hasAll判断
|
||||
if(maskSegments){
|
||||
for (let i = 0; i < Math.min(maskSegmentsLength,bitsSegments.length); i++) {
|
||||
if((maskSegments[i][SegmentPart.LOW] & bitsSegments[i][SegmentPart.LOW]) !== bitsSegments[i][SegmentPart.LOW] || (maskSegments[i][SegmentPart.HIGH] & bitsSegments[i][SegmentPart.HIGH]) !== bitsSegments[i][SegmentPart.HIGH]){
|
||||
const maskSeg = maskSegments[i]!;
|
||||
const bitsSeg = bitsSegments[i]!;
|
||||
if((maskSeg[SegmentPart.LOW] & bitsSeg[SegmentPart.LOW]) !== bitsSeg[SegmentPart.LOW] || (maskSeg[SegmentPart.HIGH] & bitsSeg[SegmentPart.HIGH]) !== bitsSeg[SegmentPart.HIGH]){
|
||||
// 存在不匹配的位,直接短路
|
||||
return false;
|
||||
}
|
||||
@@ -97,7 +102,8 @@ export class BitMask64Utils {
|
||||
}
|
||||
// 对mask中不存在,但bits中存在的区段,进行isZero判断
|
||||
for (let i = maskSegmentsLength; i < bitsSegments.length; i++) {
|
||||
if(bitsSegments[i][SegmentPart.LOW] !== 0 || bitsSegments[i][SegmentPart.HIGH] !== 0){
|
||||
const bitsSeg = bitsSegments[i]!;
|
||||
if(bitsSeg[SegmentPart.LOW] !== 0 || bitsSeg[SegmentPart.HIGH] !== 0){
|
||||
// 存在不为0的区段,直接短路
|
||||
return false;
|
||||
}
|
||||
@@ -120,7 +126,11 @@ export class BitMask64Utils {
|
||||
//不含扩展区域,或基础区域就包含指定的位,或bits不含拓展区段,直接短路。
|
||||
if(!maskSegments || !baseHasNone || !bitsSegments) return baseHasNone;
|
||||
// 额外检查扩展区域是否都包含指定的位 - 此时bitsSegments存在,如果bitsSegments[index]不存在,会被转为NaN,NaN的位运算始终返回0
|
||||
return maskSegments.every((seg, index) => (seg[SegmentPart.LOW] & bitsSegments[index][SegmentPart.LOW]) === 0 && (seg[SegmentPart.HIGH] & bitsSegments[index][SegmentPart.HIGH]) === 0);
|
||||
return maskSegments.every((seg, index) => {
|
||||
const bitsSeg = bitsSegments[index];
|
||||
if (!bitsSeg) return true;
|
||||
return (seg[SegmentPart.LOW] & bitsSeg[SegmentPart.LOW]) === 0 && (seg[SegmentPart.HIGH] & bitsSeg[SegmentPart.HIGH]) === 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +170,7 @@ export class BitMask64Utils {
|
||||
}else if(!aSeg && bSeg){
|
||||
//aSeg不存在,则必须要求bSeg全为0
|
||||
if(bSeg[SegmentPart.LOW] !== 0 || bSeg[SegmentPart.HIGH] !== 0) return false;
|
||||
}else{
|
||||
}else if(aSeg && bSeg){
|
||||
//理想状态:aSeg/bSeg都存在
|
||||
if(aSeg[SegmentPart.LOW] !== bSeg[SegmentPart.LOW] || aSeg[SegmentPart.HIGH] !== bSeg[SegmentPart.HIGH]) return false;
|
||||
}
|
||||
@@ -248,8 +258,10 @@ export class BitMask64Utils {
|
||||
|
||||
// 对每个段执行或操作
|
||||
for (let i = 0; i < otherSegments.length; i++) {
|
||||
targetSegments[i][SegmentPart.LOW] |= otherSegments[i][SegmentPart.LOW];
|
||||
targetSegments[i][SegmentPart.HIGH] |= otherSegments[i][SegmentPart.HIGH];
|
||||
const targetSeg = targetSegments[i]!;
|
||||
const otherSeg = otherSegments[i]!;
|
||||
targetSeg[SegmentPart.LOW] |= otherSeg[SegmentPart.LOW];
|
||||
targetSeg[SegmentPart.HIGH] |= otherSeg[SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,8 +289,10 @@ export class BitMask64Utils {
|
||||
|
||||
// 对每个段执行与操作
|
||||
for (let i = 0; i < otherSegments.length; i++) {
|
||||
targetSegments[i][SegmentPart.LOW] &= otherSegments[i][SegmentPart.LOW];
|
||||
targetSegments[i][SegmentPart.HIGH] &= otherSegments[i][SegmentPart.HIGH];
|
||||
const targetSeg = targetSegments[i]!;
|
||||
const otherSeg = otherSegments[i]!;
|
||||
targetSeg[SegmentPart.LOW] &= otherSeg[SegmentPart.LOW];
|
||||
targetSeg[SegmentPart.HIGH] &= otherSeg[SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,8 +319,10 @@ export class BitMask64Utils {
|
||||
|
||||
// 对每个段执行异或操作
|
||||
for (let i = 0; i < otherSegments.length; i++) {
|
||||
targetSegments[i][SegmentPart.LOW] ^= otherSegments[i][SegmentPart.LOW];
|
||||
targetSegments[i][SegmentPart.HIGH] ^= otherSegments[i][SegmentPart.HIGH];
|
||||
const targetSeg = targetSegments[i]!;
|
||||
const otherSeg = otherSegments[i]!;
|
||||
targetSeg[SegmentPart.LOW] ^= otherSeg[SegmentPart.LOW];
|
||||
targetSeg[SegmentPart.HIGH] ^= otherSeg[SegmentPart.HIGH];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,10 +333,12 @@ export class BitMask64Utils {
|
||||
public static clear(mask: BitMask64Data): void {
|
||||
mask.base[SegmentPart.LOW] = 0;
|
||||
mask.base[SegmentPart.HIGH] = 0;
|
||||
for (let i = 0; i < (mask.segments?.length ?? 0); i++) {
|
||||
const seg = mask.segments![i];
|
||||
seg[SegmentPart.LOW] = 0;
|
||||
seg[SegmentPart.HIGH] = 0;
|
||||
if (mask.segments) {
|
||||
for (let i = 0; i < mask.segments.length; i++) {
|
||||
const seg = mask.segments[i]!;
|
||||
seg[SegmentPart.LOW] = 0;
|
||||
seg[SegmentPart.HIGH] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,9 +364,11 @@ export class BitMask64Utils {
|
||||
target.segments.push([0,0]);
|
||||
}
|
||||
// 逐个重写
|
||||
for (let i = 0; i < length; i++) {
|
||||
const targetSeg = target.segments![i];
|
||||
const sourSeg = source.segments![i];
|
||||
const targetSegments = target.segments;
|
||||
const sourceSegments = source.segments;
|
||||
for (let i = 0; i < sourceSegments.length; i++) {
|
||||
const targetSeg = targetSegments[i]!;
|
||||
const sourSeg = sourceSegments[i]!;
|
||||
targetSeg[SegmentPart.LOW] = sourSeg[SegmentPart.LOW];
|
||||
targetSeg[SegmentPart.HIGH] = sourSeg[SegmentPart.HIGH];
|
||||
}
|
||||
@@ -362,7 +382,7 @@ export class BitMask64Utils {
|
||||
public static clone(mask: BitMask64Data): BitMask64Data {
|
||||
return {
|
||||
base: mask.base.slice() as BitMask64Segment,
|
||||
segments: mask.segments ? mask.segments.map(seg => [...seg]) : undefined
|
||||
...(mask.segments && { segments: mask.segments.map(seg => [...seg] as BitMask64Segment) })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -393,7 +413,7 @@ export class BitMask64Utils {
|
||||
|
||||
for (let i = -1; i < totalLength; i++) {
|
||||
let segResult = '';
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i];
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i]!;
|
||||
let hi = bitMaskData[SegmentPart.HIGH];
|
||||
let lo = bitMaskData[SegmentPart.LOW];
|
||||
if(radix == 2){
|
||||
@@ -429,7 +449,7 @@ export class BitMask64Utils {
|
||||
public static popCount(mask: BitMask64Data): number {
|
||||
let count = 0;
|
||||
for (let i = -1; i < (mask.segments?.length ?? 0); i++) {
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i];
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i]!;
|
||||
let lo = bitMaskData[SegmentPart.LOW];
|
||||
let hi = bitMaskData[SegmentPart.HIGH];
|
||||
while (lo) {
|
||||
@@ -470,7 +490,7 @@ export class BitMask64Utils {
|
||||
segments.push([0, 0]);
|
||||
}
|
||||
}
|
||||
return segments[targetSegIndex];
|
||||
return segments[targetSegIndex] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,8 @@ export class BitMaskHashMap<T> {
|
||||
|
||||
// 查找是否存在 secondaryHash
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i][0] === secondary) {
|
||||
bucket[i][1] = value;
|
||||
if (bucket[i]![0] === secondary) {
|
||||
bucket[i]![1] = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -90,8 +90,8 @@ export class BitMaskHashMap<T> {
|
||||
const bucket = this.buckets.get(primary);
|
||||
if (!bucket) return undefined;
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i][0] === secondary) {
|
||||
return bucket[i][1];
|
||||
if (bucket[i]![0] === secondary) {
|
||||
return bucket[i]![1];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -106,7 +106,7 @@ export class BitMaskHashMap<T> {
|
||||
const bucket = this.buckets.get(primary);
|
||||
if (!bucket) return false;
|
||||
for (let i = 0; i < bucket.length; i++) {
|
||||
if (bucket[i][0] === secondary) {
|
||||
if (bucket[i]![0] === secondary) {
|
||||
bucket.splice(i, 1);
|
||||
this._size--;
|
||||
if (bucket.length === 0) {
|
||||
@@ -125,7 +125,7 @@ export class BitMaskHashMap<T> {
|
||||
|
||||
*entries(): IterableIterator<[BitMask64Data, T]> {
|
||||
for (const [_, bucket] of this.buckets) {
|
||||
for (const [secondary, value] of bucket) {
|
||||
for (const [_secondary, value] of bucket) {
|
||||
// 无法还原原始 key(只存二级 hash),所以 entries 返回不了 key
|
||||
yield [undefined as any, value];
|
||||
}
|
||||
|
||||
@@ -265,16 +265,16 @@ export class Bits {
|
||||
*/
|
||||
public static fromBinaryString(binaryString: string): Bits {
|
||||
const cleanString = binaryString.replace(/\s/g, '');
|
||||
let data: BitMask64Data = { base: undefined!, segments: undefined};
|
||||
let data: BitMask64Data;
|
||||
if (cleanString.length <= 32) {
|
||||
const num = parseInt(cleanString, 2);
|
||||
data.base = [num >>> 0, 0];
|
||||
data = { base: [num >>> 0, 0] };
|
||||
} else {
|
||||
const loBits = cleanString.substring(cleanString.length - 32);
|
||||
const hiBits = cleanString.substring(0, cleanString.length - 32);
|
||||
const lo = parseInt(loBits, 2);
|
||||
const hi = parseInt(hiBits, 2);
|
||||
data.base = [lo >>> 0, hi >>> 0];
|
||||
data = { base: [lo >>> 0, hi >>> 0] };
|
||||
}
|
||||
return new Bits(data);
|
||||
}
|
||||
@@ -286,16 +286,16 @@ export class Bits {
|
||||
*/
|
||||
public static fromHexString(hexString: string): Bits {
|
||||
const cleanString = hexString.replace(/^0x/i, '');
|
||||
let data: BitMask64Data = { base: undefined!, segments: undefined};
|
||||
let data: BitMask64Data;
|
||||
if (cleanString.length <= 8) {
|
||||
const num = parseInt(cleanString, 16);
|
||||
data.base = [num >>> 0, 0];
|
||||
data = { base: [num >>> 0, 0] };
|
||||
} else {
|
||||
const loBits = cleanString.substring(cleanString.length - 8);
|
||||
const hiBits = cleanString.substring(0, cleanString.length - 8);
|
||||
const lo = parseInt(loBits, 16);
|
||||
const hi = parseInt(hiBits, 16);
|
||||
data.base = [lo >>> 0, hi >>> 0];
|
||||
data = { base: [lo >>> 0, hi >>> 0] };
|
||||
}
|
||||
return new Bits(data);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { IPoolable } from '../../Utils/Pool/IPoolable';
|
||||
* 实现IPoolable接口,支持对象池复用以减少内存分配开销。
|
||||
*/
|
||||
class PoolableEntitySet extends Set<Entity> implements IPoolable {
|
||||
constructor(...args: unknown[]) {
|
||||
constructor(..._args: unknown[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export class ComponentSparseSet {
|
||||
const lastIndex = this._componentMasks.length - 1;
|
||||
if (entityIndex !== lastIndex) {
|
||||
// 将最后一个位掩码移动到当前位置
|
||||
this._componentMasks[entityIndex] = this._componentMasks[lastIndex];
|
||||
this._componentMasks[entityIndex] = this._componentMasks[lastIndex]!;
|
||||
}
|
||||
this._componentMasks.pop();
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export class ComponentSparseSet {
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
return this.queryByComponent(componentTypes[0]);
|
||||
return this.queryByComponent(componentTypes[0]!);
|
||||
}
|
||||
|
||||
// 构建目标位掩码
|
||||
@@ -182,7 +182,7 @@ export class ComponentSparseSet {
|
||||
|
||||
// 遍历所有实体,检查位掩码匹配
|
||||
this._entities.forEach((entity, index) => {
|
||||
const entityMask = this._componentMasks[index];
|
||||
const entityMask = this._componentMasks[index]!;
|
||||
if (BitMask64Utils.hasAll(entityMask, targetMask)) {
|
||||
result.add(entity);
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export class ComponentSparseSet {
|
||||
}
|
||||
|
||||
if (componentTypes.length === 1) {
|
||||
return this.queryByComponent(componentTypes[0]);
|
||||
return this.queryByComponent(componentTypes[0]!);
|
||||
}
|
||||
|
||||
// 构建目标位掩码
|
||||
@@ -225,7 +225,7 @@ export class ComponentSparseSet {
|
||||
|
||||
// 遍历所有实体,检查位掩码匹配
|
||||
this._entities.forEach((entity, index) => {
|
||||
const entityMask = this._componentMasks[index];
|
||||
const entityMask = this._componentMasks[index]!;
|
||||
if (BitMask64Utils.hasAny(entityMask, targetMask)) {
|
||||
result.add(entity);
|
||||
}
|
||||
@@ -251,9 +251,9 @@ export class ComponentSparseSet {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entityMask = this._componentMasks[entityIndex];
|
||||
const entityMask = this._componentMasks[entityIndex]!;
|
||||
const componentMask = ComponentRegistry.getBitMask(componentType);
|
||||
|
||||
|
||||
return BitMask64Utils.hasAny(entityMask, componentMask);
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ export class ComponentSparseSet {
|
||||
*/
|
||||
public forEach(callback: (entity: Entity, mask: BitMask64Data, index: number) => void): void {
|
||||
this._entities.forEach((entity, index) => {
|
||||
callback(entity, this._componentMasks[index], index);
|
||||
callback(entity, this._componentMasks[index]!, index);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ export class EntityList {
|
||||
const idsToRecycle: number[] = [];
|
||||
|
||||
for (let i = this.buffer.length - 1; i >= 0; i--) {
|
||||
idsToRecycle.push(this.buffer[i].id);
|
||||
this.buffer[i].destroy();
|
||||
idsToRecycle.push(this.buffer[i]!.id);
|
||||
this.buffer[i]!.destroy();
|
||||
}
|
||||
|
||||
// 批量回收ID
|
||||
@@ -142,7 +142,7 @@ export class EntityList {
|
||||
*/
|
||||
public findEntity(name: string): Entity | null {
|
||||
const entities = this._nameToEntities.get(name);
|
||||
return entities && entities.length > 0 ? entities[0] : null;
|
||||
return entities && entities.length > 0 ? entities[0]! : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -209,9 +209,9 @@ export class Matcher {
|
||||
all: [...this.condition.all],
|
||||
any: [...this.condition.any],
|
||||
none: [...this.condition.none],
|
||||
tag: this.condition.tag,
|
||||
name: this.condition.name,
|
||||
component: this.condition.component
|
||||
...(this.condition.tag !== undefined && { tag: this.condition.tag }),
|
||||
...(this.condition.name !== undefined && { name: this.condition.name }),
|
||||
...(this.condition.component !== undefined && { component: this.condition.component })
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export class SparseSet<T> {
|
||||
|
||||
// 如果不是最后一个元素,则与最后一个元素交换
|
||||
if (index !== lastIndex) {
|
||||
const lastItem = this._dense[lastIndex];
|
||||
const lastItem = this._dense[lastIndex]!;
|
||||
this._dense[index] = lastItem;
|
||||
this._sparse.set(lastItem, index);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ export class SparseSet<T> {
|
||||
*/
|
||||
public forEach(callback: (item: T, index: number) => void): void {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
callback(this._dense[i], i);
|
||||
callback(this._dense[i]!, i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export class SparseSet<T> {
|
||||
public map<U>(callback: (item: T, index: number) => U): U[] {
|
||||
const result: U[] = [];
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
result.push(callback(this._dense[i], i));
|
||||
result.push(callback(this._dense[i]!, i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -167,8 +167,8 @@ export class SparseSet<T> {
|
||||
public filter(predicate: (item: T, index: number) => boolean): T[] {
|
||||
const result: T[] = [];
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (predicate(this._dense[i], i)) {
|
||||
result.push(this._dense[i]);
|
||||
if (predicate(this._dense[i]!, i)) {
|
||||
result.push(this._dense[i]!);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -182,7 +182,7 @@ export class SparseSet<T> {
|
||||
*/
|
||||
public find(predicate: (item: T, index: number) => boolean): T | undefined {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (predicate(this._dense[i], i)) {
|
||||
if (predicate(this._dense[i]!, i)) {
|
||||
return this._dense[i];
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export class SparseSet<T> {
|
||||
*/
|
||||
public some(predicate: (item: T, index: number) => boolean): boolean {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (predicate(this._dense[i], i)) {
|
||||
if (predicate(this._dense[i]!, i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export class SparseSet<T> {
|
||||
*/
|
||||
public every(predicate: (item: T, index: number) => boolean): boolean {
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
if (!predicate(this._dense[i], i)) {
|
||||
if (!predicate(this._dense[i]!, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,7 @@ export class SparseSet<T> {
|
||||
|
||||
// 检查映射关系的正确性
|
||||
for (let i = 0; i < this._dense.length; i++) {
|
||||
const item = this._dense[i];
|
||||
const item = this._dense[i]!;
|
||||
const mappedIndex = this._sparse.get(item);
|
||||
if (mappedIndex !== i) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user